From ded43d9d6903456e3a4bc5743b2370289e8edb52 Mon Sep 17 00:00:00 2001 From: "Matthias S. Benkmann" Date: Sun, 7 Dec 2014 17:03:07 +0100 Subject: [PATCH 1/5] HTTP PUT support for static.py change host default to 127.0.0.1 for better security improved option parsing --- static.py | 114 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/static.py b/static.py index eb864596..ba3df3dd 100755 --- a/static.py +++ b/static.py @@ -33,6 +33,7 @@ import string import sys from os import path, stat, getcwd from wsgiref import util +from wsgiref.validate import validator from wsgiref.headers import Headers from wsgiref.simple_server import make_server from optparse import OptionParser @@ -84,6 +85,7 @@ class Cling(object): not_modified = StatusApp('304 Not Modified', "") moved_permanently = StatusApp('301 Moved Permanently') method_not_allowed = StatusApp('405 Method Not Allowed') + success_no_content = StatusApp('204 No Content', "") def __init__(self, root, **kw): """Just set the root and any other attribs passes via **kw.""" @@ -93,9 +95,6 @@ class Cling(object): def __call__(self, environ, start_response): """Respond to a request when called in the usual WSGI way.""" - if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): - headers = [('Allow', 'GET, HEAD')] - return self.method_not_allowed(environ, start_response, headers) path_info = environ.get('PATH_INFO', '') full_path = self._full_path(path_info) if not self._is_under_root(full_path): @@ -109,6 +108,17 @@ class Cling(object): return self.moved_permanently(environ, start_response, headers) else: full_path = self._full_path(path_info + self.index_file) + try: + sz = int(environ['CONTENT_LENGTH']) + except: + sz = 0 + if environ['REQUEST_METHOD'] == 'PUT' and path_info in self.puttable and sz > 0: + data = environ['wsgi.input'].read(sz) + with open(full_path, "wb") as f: f.write(data) + return self.success_no_content(environ, start_response) + if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): + headers = [('Allow', 'GET, HEAD')] + return self.method_not_allowed(environ, start_response, headers) content_type = self._guess_type(full_path) try: etag, last_modified = self._conditions(full_path, environ) @@ -191,52 +201,74 @@ def cling_wrap(package_name, dir_name, **kw): def command(): - parser = OptionParser(usage="%prog DIR [HOST][:][PORT]", - version="static 0.3.6") + usage = "%prog [--help] [-d DIR] [-l [HOST][:PORT]] [-p RELPATH[,RELPATH...]]" + parser = OptionParser(usage=usage, version="static 0.3.6") + parser.add_option("-d", "--dir", dest="rootdir", default=".", help="Root directory to serve. Defaults to '.' .", metavar="DIR") + parser.add_option("-l", "--listen", dest="listen", default="127.0.0.1:8888", help="Listen on this interface (given by its hostname or IP) and port. HOST defaults to 127.0.0.1. PORT defaults to 8888. Leave HOST empty to listen on all interfaces (SECURITY WARNING!).", metavar="[HOST][:PORT]") + parser.add_option("-p", "--puttable", dest="puttable", default="", help="Comma or space-separated list of request paths for which to permit PUT requests.", metavar="RELPATH[,RELPATH...]") + parser.add_option("--validate", dest="validate", action="store_true", default=False, help="Enable HTTP validation. You don't need this unless you're working on static.py itself.") + options, args = parser.parse_args() - if len(args) in (1, 2): - if len(args) == 2: - parts = args[1].split(":") - if len(parts) == 1: - host = parts[0] - port = None - elif len(parts) == 2: - host, port = parts - else: - sys.exit("Invalid host:port specification.") - elif len(args) == 1: - host, port = None, None - if not host: - host = '0.0.0.0' - if not port: - port = 8888 - try: - port = int(port) - except: - sys.exit("Invalid host:port specification.") - app = Cling(args[0]) - try: - make_server(host, port, app).serve_forever() - except KeyboardInterrupt, ki: - print "Cio, baby!" - except: - sys.exit("Problem initializing server.") - else: + if len(args) > 0: parser.print_help(sys.stderr) sys.exit(1) + parts = options.listen.split(":") + if len(parts) == 1: + try: # if the the listen argument consists only of a port number + port = int(parts[0]) + host = None + except: # could not parse as port number => must be a host IP or name + host = parts[0] + port = None + elif len(parts) == 2: + host, port = parts + else: + sys.exit("Invalid host:port specification.") -def test(): - from wsgiref.validate import validator - app = Cling(getcwd()) + if not host: + host = '0.0.0.0' + if not port: + port = 8888 try: - print "Serving " + getcwd() + " to http://localhost:8888" - make_server('0.0.0.0', 8888, validator(app)).serve_forever() + port = int(port) + if port <= 0 or port > 65535: raise ValueError + except: + sys.exit("Invalid host:port specification.") + + puttable = set(path.abspath(p) for p in options.puttable.replace(","," ").split()) + if puttable and host not in ('127.0.0.1', 'localhost'): + sys.exit("Permitting PUT access for non-localhost connections is unwise.") + + options.rootdir = path.abspath(options.rootdir) + + for p in puttable: + if not p.startswith(options.rootdir): + sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) + if path.exists(p) and not path.isfile(p): + sys.exit("puttable path '%s' exists but is not a file" % p) + + # cut off root prefix from puttable paths + puttable = set(p[len(options.rootdir):] for p in puttable) + + app = Cling(options.rootdir, puttable=puttable) + + if options.validate: + app = validator(app) + + try: + print "Serving %s to http://%s:%d" % (options.rootdir, host, port) + if puttable: + print "The following paths (relative to server root) may be OVERWRITTEN via HTTP PUT.\nI HOPE EVERY USER ON THIS SYSTEM IS TRUSTED!" + for p in puttable: + print p + make_server(host, port, app).serve_forever() except KeyboardInterrupt, ki: - print "" - print "Ciao, baby!" + print "Cio, baby!" + except: + sys.exit("Problem initializing server: %s" % sys.exc_info()[1]) if __name__ == '__main__': - test() + command() From 8919b113d15069f40df01f8c56d0e800946c29b8 Mon Sep 17 00:00:00 2001 From: "Matthias S. Benkmann" Date: Sun, 7 Dec 2014 17:38:15 +0100 Subject: [PATCH 2/5] add server upload buttons and Ctrl-S bindings to mode_editor in combination with PUT support in ./static.py (or whatever other web server) this allows people to save both the highlighting rules and the demo documents right from the mode editor for a better workflow. --- demo/kitchen-sink/doclist.js | 19 +++++++++++++++++- lib/ace/lib/net.js | 13 ++++++++++++ tool/mode_creator.html | 4 +++- tool/mode_creator.js | 39 ++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/demo/kitchen-sink/doclist.js b/demo/kitchen-sink/doclist.js index b0198238..567c62cd 100644 --- a/demo/kitchen-sink/doclist.js +++ b/demo/kitchen-sink/doclist.js @@ -169,13 +169,30 @@ function loadDoc(name, callback) { }); } +// callback is called with the error message from PUT (if any) +function saveDoc(name, callback) { + var doc = fileCache[name]; + if (!doc || !doc.session) + return callback("Unknown document: " + name); + + var path = doc.path; + var parts = path.split("/"); + if (parts[0] == "docs") + path = "demo/kitchen-sink/" + path; + else if (parts[0] == "ace") + path = "lib/" + path; + + net.put(path, doc.session.getValue(), callback); +} + module.exports = { fileCache: fileCache, docs: sort(prepareDocList(docs)), ownSource: prepareDocList(ownSource), hugeDocs: prepareDocList(hugeDocs), initDoc: initDoc, - loadDoc: loadDoc + loadDoc: loadDoc, + saveDoc: saveDoc, }; module.exports.all = { "Mode Examples": module.exports.docs, diff --git a/lib/ace/lib/net.js b/lib/ace/lib/net.js index bba76df8..4c07ec2b 100644 --- a/lib/ace/lib/net.js +++ b/lib/ace/lib/net.js @@ -22,6 +22,19 @@ exports.get = function (url, callback) { xhr.send(null); }; +exports.put = function (url, data, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('PUT', url, true); + xhr.onreadystatechange = function () { + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + callback(xhr.responseText); + } + }; + xhr.send(data); +}; + exports.loadScript = function(path, callback) { var head = dom.getDocumentHead(); var s = document.createElement('script'); diff --git a/tool/mode_creator.html b/tool/mode_creator.html index e2db56af..cd6b2461 100644 --- a/tool/mode_creator.html +++ b/tool/mode_creator.html @@ -19,7 +19,7 @@ border-bottom: solid 1px; } .separator-h { - padding: 0 20px; + padding: 0 10px; } #closeBtn { background: rgba(245, 146, 146, 0.5); @@ -59,6 +59,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/tool/mode_creator.js b/tool/mode_creator.js index 6b1122cd..ee235eca 100644 --- a/tool/mode_creator.js +++ b/tool/mode_creator.js @@ -53,6 +53,7 @@ util.bindDropdown("doc", function(value) { doclist.loadDoc(value, function(session) { if (session) { editor2.setSession(session); + uploadEl2.disabled = session.getUndoManager().isClean(); } }); }); @@ -60,6 +61,7 @@ util.bindDropdown("doc", function(value) { var modeEl = document.getElementById("modeEl"); util.fillDropdown(modeEl, modelist.modes); var modeSessions = {}; +var savedLeadingComments = ""; util.bindDropdown(modeEl, function(value) { if (modeSessions[value]) { editor1.setSession(modeSessions[value]); @@ -68,7 +70,10 @@ util.bindDropdown(modeEl, function(value) { } var hp = "./lib/ace/mode/" + value + "_highlight_rules.js"; net.get(hp, function(text) { + uploadEl1.disabled = true; + savedLeadingComments = text; text = util.stripLeadingComments(text); + savedLeadingComments = savedLeadingComments.substr(0, savedLeadingComments.length - text.length); var session = new EditSession(text); session.setUndoManager(new UndoManager()); @@ -85,6 +90,40 @@ document.getElementById("syncToMode").onclick = function() { docEl.onchange(); run(); }; + +var uploadEl1 = document.getElementById("uploadToServer1"); +var uploadEl2 = document.getElementById("uploadToServer2"); +uploadEl1.onclick = function() { + var text = savedLeadingComments + editor1.getValue(); + var url = "./lib/ace/mode/" + modeEl.value + "_highlight_rules.js"; + net.put(url, text, function(text) { + if (text.trim().length > 0) + log(text); + else { + uploadEl1.disabled = true; + editor1.getSession().getUndoManager().markClean(); + } + }); +}; +editor1.commands.bindKey("Ctrl-S", uploadEl1.onclick); +uploadEl2.onclick = function() { + doclist.saveDoc(docEl.value, function(text) { + if (text.trim().length > 0) + log(text); + else { + uploadEl2.disabled = true; + editor2.getSession().getUndoManager().markClean(); + } + }); +}; +editor2.commands.bindKey("Ctrl-S", uploadEl2.onclick); +editor1.on('change', function() { + uploadEl1.disabled = false; +}); +editor2.on('change', function() { + uploadEl2.disabled = false; +}); + document.getElementById("perfTest").onclick = function() { var lines = editor2.session.doc.getAllLines(); if (!lines.length) From e7afb4f19ea7690523436e3f025cd0bf0f528490 Mon Sep 17 00:00:00 2001 From: "Matthias S. Benkmann" Date: Tue, 23 Dec 2014 15:51:23 +0100 Subject: [PATCH 3/5] support glob patterns for --puttable; reformat for ancient terminals --- static.py | 57 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/static.py b/static.py index ba3df3dd..913648e2 100755 --- a/static.py +++ b/static.py @@ -32,6 +32,7 @@ import time import string import sys from os import path, stat, getcwd +from fnmatch import fnmatch from wsgiref import util from wsgiref.validate import validator from wsgiref.headers import Headers @@ -86,6 +87,7 @@ class Cling(object): moved_permanently = StatusApp('301 Moved Permanently') method_not_allowed = StatusApp('405 Method Not Allowed') success_no_content = StatusApp('204 No Content', "") + server_error = StatusApp('500 Internal Server Error') def __init__(self, root, **kw): """Just set the root and any other attribs passes via **kw.""" @@ -112,10 +114,16 @@ class Cling(object): sz = int(environ['CONTENT_LENGTH']) except: sz = 0 - if environ['REQUEST_METHOD'] == 'PUT' and path_info in self.puttable and sz > 0: - data = environ['wsgi.input'].read(sz) - with open(full_path, "wb") as f: f.write(data) - return self.success_no_content(environ, start_response) + if environ['REQUEST_METHOD'] == 'PUT' and sz > 0: + for putglob in self.puttable: + if fnmatch(path_info, putglob): + data = environ['wsgi.input'].read(sz) + try: + with open(full_path, "wb") as f: f.write(data) + return self.success_no_content(environ, start_response) + except: + print sys.exc_info()[1] + return self.server_error(environ, start_response) if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): headers = [('Allow', 'GET, HEAD')] return self.method_not_allowed(environ, start_response, headers) @@ -201,12 +209,28 @@ def cling_wrap(package_name, dir_name, **kw): def command(): - usage = "%prog [--help] [-d DIR] [-l [HOST][:PORT]] [-p RELPATH[,RELPATH...]]" + usage = "%prog [--help] [-d DIR] [-l [HOST][:PORT]] [-p GLOB[,GLOB...]]" parser = OptionParser(usage=usage, version="static 0.3.6") - parser.add_option("-d", "--dir", dest="rootdir", default=".", help="Root directory to serve. Defaults to '.' .", metavar="DIR") - parser.add_option("-l", "--listen", dest="listen", default="127.0.0.1:8888", help="Listen on this interface (given by its hostname or IP) and port. HOST defaults to 127.0.0.1. PORT defaults to 8888. Leave HOST empty to listen on all interfaces (SECURITY WARNING!).", metavar="[HOST][:PORT]") - parser.add_option("-p", "--puttable", dest="puttable", default="", help="Comma or space-separated list of request paths for which to permit PUT requests.", metavar="RELPATH[,RELPATH...]") - parser.add_option("--validate", dest="validate", action="store_true", default=False, help="Enable HTTP validation. You don't need this unless you're working on static.py itself.") + parser.add_option("-d", "--dir", dest="rootdir", default=".", + help="Root directory to serve. Defaults to '.' .", metavar="DIR") + parser.add_option("-l", "--listen", dest="listen", default="127.0.0.1:8888", + help="Listen on this interface (given by its hostname or IP) and port."+ + " HOST defaults to 127.0.0.1. PORT defaults to 8888. "+ + "Leave HOST empty to listen on all interfaces (INSECURE!).", + metavar="[HOST][:PORT]") + parser.add_option("-p", "--puttable", dest="puttable", default="", + help="Comma or space-separated list of request paths for which to"+ + " permit PUT requests. Each path is a glob pattern that may "+ + "contain wildcard characters '*' and/or '?'. "+ + "'*' matches any sequence of characters, including the empty"+ + " string. '?' matches exactly 1 arbitrary character. "+ + "NOTE: Both '*' and '?' match slashes and dots. "+ + "I.e. --puttable=* makes every file under DIR writable!", + metavar="GLOB[,GLOB...]") + parser.add_option("--validate", dest="validate", action="store_true", + default=False, + help="Enable HTTP validation. You don't need this unless "+ + "you're developing static.py itself.") options, args = parser.parse_args() if len(args) > 0: @@ -236,17 +260,16 @@ def command(): except: sys.exit("Invalid host:port specification.") - puttable = set(path.abspath(p) for p in options.puttable.replace(","," ").split()) + puttable = set(path.abspath(p) for p in + options.puttable.replace(","," ").split()) if puttable and host not in ('127.0.0.1', 'localhost'): - sys.exit("Permitting PUT access for non-localhost connections is unwise.") + sys.exit("Permitting PUT access for non-localhost connections is unwise.") options.rootdir = path.abspath(options.rootdir) for p in puttable: - if not p.startswith(options.rootdir): - sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) - if path.exists(p) and not path.isfile(p): - sys.exit("puttable path '%s' exists but is not a file" % p) + if not p.startswith(options.rootdir): + sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) # cut off root prefix from puttable paths puttable = set(p[len(options.rootdir):] for p in puttable) @@ -259,7 +282,9 @@ def command(): try: print "Serving %s to http://%s:%d" % (options.rootdir, host, port) if puttable: - print "The following paths (relative to server root) may be OVERWRITTEN via HTTP PUT.\nI HOPE EVERY USER ON THIS SYSTEM IS TRUSTED!" + print("The following paths (relative to server root) may be "+ + "OVERWRITTEN via HTTP PUT.\n"+ + "I HOPE EVERY USER ON THIS SYSTEM IS TRUSTED!") for p in puttable: print p make_server(host, port, app).serve_forever() From eccc98b83b3c310860f69b6f78890b26bea3b254 Mon Sep 17 00:00:00 2001 From: "Matthias S. Benkmann" Date: Tue, 23 Dec 2014 16:33:56 +0100 Subject: [PATCH 4/5] net.put => net.request('PUT'; improved usability for "Save" buttons --- demo/kitchen-sink/doclist.js | 2 +- lib/ace/lib/net.js | 21 ++++++--------------- tool/mode_creator.html | 4 ++-- tool/mode_creator.js | 32 +++++++++++++++++++------------- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/demo/kitchen-sink/doclist.js b/demo/kitchen-sink/doclist.js index 567c62cd..abdc9a3e 100644 --- a/demo/kitchen-sink/doclist.js +++ b/demo/kitchen-sink/doclist.js @@ -182,7 +182,7 @@ function saveDoc(name, callback) { else if (parts[0] == "ace") path = "lib/" + path; - net.put(path, doc.session.getValue(), callback); + net.request('PUT', path, doc.session.getValue(), callback); } module.exports = { diff --git a/lib/ace/lib/net.js b/lib/ace/lib/net.js index 4c07ec2b..357fcb9d 100644 --- a/lib/ace/lib/net.js +++ b/lib/ace/lib/net.js @@ -9,22 +9,9 @@ define(function(require, exports, module) { "use strict"; var dom = require("./dom"); -exports.get = function (url, callback) { +exports.request = function (verb, url, data, callback) { var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - //Do not explicitly handle errors, those should be - //visible via console output in the browser. - if (xhr.readyState === 4) { - callback(xhr.responseText); - } - }; - xhr.send(null); -}; - -exports.put = function (url, data, callback) { - var xhr = new XMLHttpRequest(); - xhr.open('PUT', url, true); + xhr.open(verb, url, true); xhr.onreadystatechange = function () { //Do not explicitly handle errors, those should be //visible via console output in the browser. @@ -35,6 +22,10 @@ exports.put = function (url, data, callback) { xhr.send(data); }; +exports.get = function (url, callback) { + this.request('GET', url, null, callback); +}; + exports.loadScript = function(path, callback) { var head = dom.getDocumentHead(); var s = document.createElement('script'); diff --git a/tool/mode_creator.html b/tool/mode_creator.html index cd6b2461..b4c66715 100644 --- a/tool/mode_creator.html +++ b/tool/mode_creator.html @@ -59,7 +59,7 @@ - + @@ -71,7 +71,7 @@ - + diff --git a/tool/mode_creator.js b/tool/mode_creator.js index ee235eca..c78345ac 100644 --- a/tool/mode_creator.js +++ b/tool/mode_creator.js @@ -96,24 +96,14 @@ var uploadEl2 = document.getElementById("uploadToServer2"); uploadEl1.onclick = function() { var text = savedLeadingComments + editor1.getValue(); var url = "./lib/ace/mode/" + modeEl.value + "_highlight_rules.js"; - net.put(url, text, function(text) { - if (text.trim().length > 0) - log(text); - else { - uploadEl1.disabled = true; - editor1.getSession().getUndoManager().markClean(); - } + net.request('PUT', url, text, function(text) { + handle_put_result(text, editor1, uploadEl1); }); }; editor1.commands.bindKey("Ctrl-S", uploadEl1.onclick); uploadEl2.onclick = function() { doclist.saveDoc(docEl.value, function(text) { - if (text.trim().length > 0) - log(text); - else { - uploadEl2.disabled = true; - editor2.getSession().getUndoManager().markClean(); - } + handle_put_result(text, editor2, uploadEl2); }); }; editor2.commands.bindKey("Ctrl-S", uploadEl2.onclick); @@ -124,6 +114,22 @@ editor2.on('change', function() { uploadEl2.disabled = false; }); +function handle_put_result(text, editor, buttonEl) { + text = text.trim(); + if (text.length == 0) { + buttonEl.disabled = true; + editor.getSession().getUndoManager().markClean(); + } else { + if (text.indexOf("405") == 0) { + log("Write access to this file is disabled.\n"+ + "To enable saving your changes to disk, clone the Ace repository"+ + "\nand run the included static.py web server with the option\n"+ + "--puttable='lib/ace/mode/*_highlight_rules.js,demo/kitchen-sink/docs/*'"); + } else + log(text); + } +} + document.getElementById("perfTest").onclick = function() { var lines = editor2.session.doc.getAllLines(); if (!lines.length) From 0054bb87946e27d534b49fb1cdf1acf1b261a888 Mon Sep 17 00:00:00 2001 From: nightwing Date: Sun, 4 Jan 2015 02:13:28 +0400 Subject: [PATCH 5/5] cleanup --- demo/kitchen-sink/doclist.js | 20 ++++- lib/ace/lib/net.js | 10 +-- lib/ace/mode/abap_highlight_rules.js | 2 +- static.js | 113 +++++++++++++++++++++------ static.py | 11 ++- tool/mode_creator.html | 4 +- tool/mode_creator.js | 92 ++++++++++++---------- 7 files changed, 165 insertions(+), 87 deletions(-) diff --git a/demo/kitchen-sink/doclist.js b/demo/kitchen-sink/doclist.js index abdc9a3e..a4848d50 100644 --- a/demo/kitchen-sink/doclist.js +++ b/demo/kitchen-sink/doclist.js @@ -169,9 +169,8 @@ function loadDoc(name, callback) { }); } -// callback is called with the error message from PUT (if any) function saveDoc(name, callback) { - var doc = fileCache[name]; + var doc = fileCache[name] || name; if (!doc || !doc.session) return callback("Unknown document: " + name); @@ -182,9 +181,24 @@ function saveDoc(name, callback) { else if (parts[0] == "ace") path = "lib/" + path; - net.request('PUT', path, doc.session.getValue(), callback); + upload(path, doc.session.getValue(), callback); } +function upload(url, data, callback) { + url = net.qualifyURL(url); + if (!/https?:/.test(url)) + return callback(new Error("Unsupported url scheme")); + var xhr = new XMLHttpRequest(); + xhr.open("PUT", url, true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + callback(!/^2../.test(xhr.status)); + } + }; + xhr.send(data); +}; + + module.exports = { fileCache: fileCache, docs: sort(prepareDocList(docs)), diff --git a/lib/ace/lib/net.js b/lib/ace/lib/net.js index 357fcb9d..bba76df8 100644 --- a/lib/ace/lib/net.js +++ b/lib/ace/lib/net.js @@ -9,9 +9,9 @@ define(function(require, exports, module) { "use strict"; var dom = require("./dom"); -exports.request = function (verb, url, data, callback) { +exports.get = function (url, callback) { var xhr = new XMLHttpRequest(); - xhr.open(verb, url, true); + xhr.open('GET', url, true); xhr.onreadystatechange = function () { //Do not explicitly handle errors, those should be //visible via console output in the browser. @@ -19,11 +19,7 @@ exports.request = function (verb, url, data, callback) { callback(xhr.responseText); } }; - xhr.send(data); -}; - -exports.get = function (url, callback) { - this.request('GET', url, null, callback); + xhr.send(null); }; exports.loadScript = function(path, callback) { diff --git a/lib/ace/mode/abap_highlight_rules.js b/lib/ace/mode/abap_highlight_rules.js index 6b232c90..bab1e504 100644 --- a/lib/ace/mode/abap_highlight_rules.js +++ b/lib/ace/mode/abap_highlight_rules.js @@ -126,7 +126,7 @@ var AbapHighlightRules = function() { {token : "string", regex : "`", next : "start"}, {defaultToken : "string"} ] - } + }; }; oop.inherits(AbapHighlightRules, TextHighlightRules); diff --git a/static.js b/static.js index 4f7e7248..a711715e 100755 --- a/static.js +++ b/static.js @@ -10,37 +10,100 @@ var http = require("http") // compatibility with node 0.6 if (!fs.exists) - fs.exists = path.exists; + fs.exists = path.exists; -http.createServer(function(request, response) { +var allowSave = process.argv.indexOf("--allow-save") != -1; - var uri = url.parse(request.url).pathname - , filename = path.join(process.cwd(), uri); - - fs.exists(filename, function(exists) { - if(!exists) { - response.writeHead(404, {"Content-Type": "text/plain"}); - response.write("404 Not Found\n"); - response.end(); - return; +http.createServer(function(req, res) { + var uri = url.parse(req.url).pathname + , filename = path.join(process.cwd(), uri); + + if (req.method == "PUT") { + if (!allowSave) + return error(res, 404, "Saving not allowed pass --allow-save to enable"); + save(req, res, filename); } - if (fs.statSync(filename).isDirectory()) filename += '/index.html'; + fs.exists(filename, function(exists) { + if (!exists) + return error(res, 404, "404 Not Found\n"); - fs.readFile(filename, "binary", function(err, file) { - if(err) { - response.writeHead(500, {"Content-Type": "text/plain"}); - response.write(err + "\n"); - response.end(); - return; - } + if (fs.statSync(filename).isDirectory()) { + var files = fs.readdirSync(filename); + res.writeHead(200, {"Content-Type": "text/html"}); + + files.push(".", ".."); + var html = files.map(function(name) { + var href = uri + "/" + name; + href = href.replace(/[\/\\]+/g, "/").replace(/\/$/g, ""); + if (fs.statSync(filename + "/" + name + "/").isDirectory()) + href += "/"; + return "" + name + "
"; + }); - var contentType = mime.lookup(filename) || "text/plain"; - response.writeHead(200, {"Content-Type": contentType}); - response.write(file, "binary"); - response.end(); + res._hasBody && res.write(html.join("")); + res.end(); + return; + } + + fs.readFile(filename, "binary", function(err, file) { + if (err) { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.write(err + "\n"); + res.end(); + return; + } + + var contentType = mime.lookup(filename) || "text/plain"; + res.writeHead(200, { "Content-Type": contentType }); + res.write(file, "binary"); + res.end(); + }); }); - }); }).listen(port, ip); -console.log("http://localhost:" + port); +function error(res, status, message, error) { + console.error(error || message); + res.writeHead(status, { "Content-Type": "text/plain" }); + res.write(message); + res.end(); +} + +function save(req, res, filePath) { + var data = ""; + req.on("data", function(chunk) { + data += chunk; + }); + req.on("error", function() { + error(res, 404, "Could't save file"); + }); + req.on("end", function() { + try { + fs.writeFileSync(filePath, data); + } + catch (e) { + return error(res, 404, "Could't save file", e); + } + res.statusCode = 200; + res.end("OK"); + }); +} + +function getLocalIps() { + var os = require("os"); + + var interfaces = os.networkInterfaces ? os.networkInterfaces() : {}; + var addresses = []; + for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2]; + if (address.family === "IPv4" && !address.internal) { + addresses.push(address.address); + } + } + } + return addresses; +} + +console.log("http://" + (ip == "0.0.0.0" ? getLocalIps()[0] : ip) + ":" + port); + diff --git a/static.py b/static.py index 913648e2..7a2faf91 100755 --- a/static.py +++ b/static.py @@ -263,13 +263,13 @@ def command(): puttable = set(path.abspath(p) for p in options.puttable.replace(","," ").split()) if puttable and host not in ('127.0.0.1', 'localhost'): - sys.exit("Permitting PUT access for non-localhost connections is unwise.") + print("Permitting PUT access for non-localhost connections may be unwise.") options.rootdir = path.abspath(options.rootdir) for p in puttable: - if not p.startswith(options.rootdir): - sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) + if not p.startswith(options.rootdir): + sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) # cut off root prefix from puttable paths puttable = set(p[len(options.rootdir):] for p in puttable) @@ -283,13 +283,12 @@ def command(): print "Serving %s to http://%s:%d" % (options.rootdir, host, port) if puttable: print("The following paths (relative to server root) may be "+ - "OVERWRITTEN via HTTP PUT.\n"+ - "I HOPE EVERY USER ON THIS SYSTEM IS TRUSTED!") + "OVERWRITTEN via HTTP PUT.") for p in puttable: print p make_server(host, port, app).serve_forever() except KeyboardInterrupt, ki: - print "Cio, baby!" + print "Ciao, baby!" except: sys.exit("Problem initializing server: %s" % sys.exc_info()[1]) diff --git a/tool/mode_creator.html b/tool/mode_creator.html index b4c66715..259ba64e 100644 --- a/tool/mode_creator.html +++ b/tool/mode_creator.html @@ -59,7 +59,7 @@ - + @@ -71,7 +71,7 @@ - + diff --git a/tool/mode_creator.js b/tool/mode_creator.js index c78345ac..e79c54c6 100644 --- a/tool/mode_creator.js +++ b/tool/mode_creator.js @@ -53,7 +53,8 @@ util.bindDropdown("doc", function(value) { doclist.loadDoc(value, function(session) { if (session) { editor2.setSession(session); - uploadEl2.disabled = session.getUndoManager().isClean(); + session.getUndoManager().markClean(); + updateSaveButtonState(null, editor2); } }); }); @@ -61,7 +62,7 @@ util.bindDropdown("doc", function(value) { var modeEl = document.getElementById("modeEl"); util.fillDropdown(modeEl, modelist.modes); var modeSessions = {}; -var savedLeadingComments = ""; + util.bindDropdown(modeEl, function(value) { if (modeSessions[value]) { editor1.setSession(modeSessions[value]); @@ -70,17 +71,18 @@ util.bindDropdown(modeEl, function(value) { } var hp = "./lib/ace/mode/" + value + "_highlight_rules.js"; net.get(hp, function(text) { - uploadEl1.disabled = true; - savedLeadingComments = text; - text = util.stripLeadingComments(text); - savedLeadingComments = savedLeadingComments.substr(0, savedLeadingComments.length - text.length); - var session = new EditSession(text); session.setUndoManager(new UndoManager()); + modeSessions[value] = session; - session.setMode("ace/mode/javascript"); + session.setMode("ace/mode/javascript", function() { + if (session.getLine(0).match(/^\s*\//)) + session.toggleFoldWidget(0); // fold licence comment + }); editor1.setSession(modeSessions[value]); + session.getUndoManager().markClean(); + updateSaveButtonState(null, editor1); schedule(); }); }); @@ -91,43 +93,47 @@ document.getElementById("syncToMode").onclick = function() { run(); }; -var uploadEl1 = document.getElementById("uploadToServer1"); -var uploadEl2 = document.getElementById("uploadToServer2"); -uploadEl1.onclick = function() { - var text = savedLeadingComments + editor1.getValue(); - var url = "./lib/ace/mode/" + modeEl.value + "_highlight_rules.js"; - net.request('PUT', url, text, function(text) { - handle_put_result(text, editor1, uploadEl1); - }); -}; -editor1.commands.bindKey("Ctrl-S", uploadEl1.onclick); -uploadEl2.onclick = function() { - doclist.saveDoc(docEl.value, function(text) { - handle_put_result(text, editor2, uploadEl2); - }); -}; -editor2.commands.bindKey("Ctrl-S", uploadEl2.onclick); -editor1.on('change', function() { - uploadEl1.disabled = false; -}); -editor2.on('change', function() { - uploadEl2.disabled = false; -}); +editor1.saveButton = document.getElementById("saveButton1"); +editor2.saveButton = document.getElementById("saveButton2"); +editor1.saveButton.editor = editor1; +editor2.saveButton.editor = editor2; -function handle_put_result(text, editor, buttonEl) { - text = text.trim(); - if (text.length == 0) { - buttonEl.disabled = true; - editor.getSession().getUndoManager().markClean(); - } else { - if (text.indexOf("405") == 0) { - log("Write access to this file is disabled.\n"+ - "To enable saving your changes to disk, clone the Ace repository"+ - "\nand run the included static.py web server with the option\n"+ - "--puttable='lib/ace/mode/*_highlight_rules.js,demo/kitchen-sink/docs/*'"); - } else - log(text); +editor1.saveButton.onclick = function() { + doclist.saveDoc({ + path: "./lib/ace/mode/" + modeEl.value + "_highlight_rules.js", + session: editor1.session + }, function(err) { + handleSaveResult(err, editor1); + }); +}; +editor1.commands.bindKey({ + win: "Ctrl-S", mac: "Cmd-s" +}, editor1.saveButton.onclick); +editor2.saveButton.onclick = function() { + doclist.saveDoc(docEl.value, function(err) { + handleSaveResult(err, editor2); + }); +}; +editor2.commands.bindKey({ + win: "Ctrl-S", mac: "Cmd-s" +}, editor2.saveButton.onclick); +function updateSaveButtonState(e, editor){ + editor.saveButton.disabled = editor.session.getUndoManager().isClean(); +} +editor1.on("input", updateSaveButtonState); +editor2.on("input", updateSaveButtonState); + +function handleSaveResult(err, editor) { + if (err) { + return log( + "Write access to this file is disabled.\n"+ + "To enable saving your changes to disk, clone the Ace repository\n"+ + "and run the included web server with the --allow-write option\n"+ + "`node static.js --allow-write` or `static.py --puttable=*`" + ); } + editor.session.getUndoManager().markClean(); + updateSaveButtonState(null, editor); } document.getElementById("perfTest").onclick = function() {