diff --git a/src/Config.py b/src/Config.py index df512c92..dad7d4e9 100644 --- a/src/Config.py +++ b/src/Config.py @@ -3,7 +3,7 @@ import ConfigParser class Config(object): def __init__(self): - self.version = "0.1.5" + self.version = "0.1.6" self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file diff --git a/src/Site/Site.py b/src/Site/Site.py index ed7a1a84..bd547b54 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -150,7 +150,7 @@ class Site: if changed_files: for changed_file in changed_files: self.bad_files[changed_file] = True - self.checkFiles(quick_check=True) # Quick check files based on file size + if not self.settings["own"]: self.checkFiles(quick_check=True) # Quick check files based on file size if self.bad_files: self.download() return changed_files @@ -457,7 +457,6 @@ class Site: self.log.info("Signing modified content.json...") sign_content = json.dumps(content, sort_keys=True) - self.log.debug("Content: %s" % sign_content) sign = CryptBitcoin.sign(sign_content, privatekey) content["sign"] = sign @@ -466,3 +465,4 @@ class Site: open("%s/content.json" % self.directory, "w").write(json.dumps(content, indent=4, sort_keys=True)) self.log.info("Site signed!") + return True diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 9779a639..a365eee3 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -47,7 +47,10 @@ def need(address, all_file=True): if not isAddress(address): raise Exception("Not address: %s" % address) logging.debug("Added new site: %s" % address) sites[address] = Site(address) - sites[address].settings["serving"] = True # Maybe it was deleted before + if not sites[address].settings["serving"]: # Maybe it was deleted before + sites[address].settings["serving"] = True + sites[address].saveSettings() + site = sites[address] if all_file: site.download() return site diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 4c57c916..63b5367e 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -66,7 +66,8 @@ class UiRequest: # Send response headers - def sendHeader(self, status=200, content_type="text/html; charset=utf-8", extra_headers=[]): + def sendHeader(self, status=200, content_type="text/html", extra_headers=[]): + if content_type == "text/html": content_type = "text/html; charset=utf-8" headers = [] headers.append(("Version", "HTTP/1.1")) headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access @@ -101,6 +102,7 @@ class UiRequest: # Render a file from media with iframe site wrapper def actionWrapper(self, path): + if "." in path and not path.endswith(".html"): return self.actionSiteMedia("/media"+path) # Only serve html files with frame if self.env.get("HTTP_X_REQUESTED_WITH"): return self.error403() # No ajax allowed on wrapper match = re.match("/(?P[A-Za-z0-9]+)(?P/.*|$)", path) @@ -109,19 +111,29 @@ class UiRequest: if not inner_path: inner_path = "index.html" # If inner path defaults to index.html site = self.server.sites.get(match.group("site")) - if site and site.content and not site.bad_files: # Its downloaded + if site and site.content and (not site.bad_files or site.settings["own"]): # Its downloaded or own title = site.content["title"] else: title = "Loading %s..." % match.group("site") site = SiteManager.need(match.group("site")) # Start download site if not site: self.error404() - self.sendHeader(extra_headers=[("X-Frame-Options", "DENY")]) + + # Wrapper variable inits + if self.env.get("QUERY_STRING"): + query_string = "?"+self.env["QUERY_STRING"] + else: + query_string = "" + body_style = "" + if site.content and site.content.get("background-color"): body_style += "background-color: "+site.content["background-color"]+";" + return self.render("src/Ui/template/wrapper.html", inner_path=inner_path, address=match.group("site"), title=title, + body_style=body_style, + query_string=query_string, wrapper_key=site.settings["wrapper_key"], permissions=json.dumps(site.settings["permissions"]), show_loadingscreen=json.dumps(not os.path.isfile(site.getPath(inner_path))), diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 44c8e593..9b52048c 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -91,6 +91,10 @@ class UiWebsocket: self.actionServerInfo(req["id"], req["params"]) elif cmd == "siteUpdate": self.actionSiteUpdate(req["id"], req["params"]) + elif cmd == "sitePublish": + self.actionSitePublish(req["id"], req["params"]) + elif cmd == "fileWrite": + self.actionFileWrite(req["id"], req["params"]) # Admin commands elif cmd == "sitePause" and "ADMIN" in permissions: self.actionSitePause(req["id"], req["params"]) @@ -173,6 +177,60 @@ class UiWebsocket: self.response(to, ret) + def actionSitePublish(self, to, params): + site = self.site + if not site.settings["own"]: return self.response(to, "Forbidden, you can only modify your own sites") + + # Signing + site.loadContent(True) # Reload content.json, ignore errors to make it up-to-date + signed = site.signContent(params[0]) # Sign using private key sent by user + if signed: + self.cmd("notification", ["done", "Private key correct, site signed!", 5000]) # Display message for 5 sec + else: + self.cmd("notification", ["error", "Site sign failed: invalid private key."]) + self.response(to, "Site sign failed") + return + site.loadContent(True) # Load new content.json, ignore errors + + # Publishing + if not site.settings["serving"]: # Enable site if paused + site.settings["serving"] = True + site.saveSettings() + site.announce() + + published = site.publish(5) # Publish to 5 peer + + if published>0: # Successfuly published + self.cmd("notification", ["done", "Site published to %s peers." % published, 5000]) + self.response(to, "ok") + site.updateWebsocket() # Send updated site data to local websocket clients + else: + if len(site.peers) == 0: + self.cmd("notification", ["info", "No peers found, but your site is ready to access."]) + self.response(to, "No peers found, but your site is ready to access.") + else: + self.cmd("notification", ["error", "Site publish failed."]) + self.response(to, "Site publish failed.") + + + + + + # Write a file to disk + def actionFileWrite(self, to, params): + if not self.site.settings["own"]: return self.response(to, "Forbidden, you can only modify your own sites") + try: + import base64 + content = base64.b64decode(params[1]) + open(self.site.getPath(params[0]), "wb").write(content) + except Exception, err: + return self.response(to, "Write error: %s" % err) + + return self.response(to, "ok") + + + + # - Admin actions - # List all site info diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index a43ddbb2..9c7b916b 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -23,6 +23,9 @@ class Wrapper @site_error = null # Latest failed file download window.onload = @onLoad # On iframe loaded + $(window).on "hashchange", -> # On hash change + src = $("#inner-iframe").attr("src").replace(/#.*/, "")+window.location.hash + $("#inner-iframe").attr("src", src) @ @@ -60,6 +63,8 @@ class Wrapper @notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2]) else if cmd == "wrapperConfirm" # Display confirm message @actionWrapperConfirm(message) + else if cmd == "wrapperPrompt" # Prompt input + @actionWrapperPrompt(message) else # Send to websocket @ws.send(message) # Pass message to websocket @@ -80,6 +85,30 @@ class Wrapper @notifications.add("notification-#{message.id}", "ask", body) + + actionWrapperPrompt: (message) -> + message.params = @toHtmlSafe(message.params) # Escape html + if message.params[1] then type = message.params[1] else type = "text" + caption = "OK" + + body = $(""+message.params[0]+"") + + input = $("") # Add input + input.on "keyup", (e) => # Send on enter + if e.keyCode == 13 + @sendInner {"cmd": "response", "to": message.id, "result": input.val()} # Response to confirm + body.append(input) + + button = $("#{caption}") # Add confirm button + button.on "click", => # Response on button click + @sendInner {"cmd": "response", "to": message.id, "result": input.val()} # Response to confirm + return false + body.append(button) + + + @notifications.add("notification-#{message.id}", "ask", body) + + onOpenWebsocket: (e) => @ws.cmd "channelJoin", {"channel": "siteChanged"} # Get info on modifications @log "onOpenWebsocket", @inner_ready, @wrapperWsInited @@ -112,10 +141,11 @@ class Wrapper # Iframe loaded onLoad: (e) => - @log "onLoad", e + @log "onLoad" @inner_loaded = true if not @inner_ready then @sendInner {"cmd": "wrapperReady"} # Inner frame loaded before wrapper if not @site_error then @loading.hideScreen() # Hide loading screen + if window.location.hash then $("#inner-iframe")[0].src += window.location.hash # Hash tag if @ws.ws.readyState == 1 and not @site_info # Ws opened @reloadSiteInfo() diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index cf50fe81..24624d53 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -35,7 +35,7 @@ a { color: black } .notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap } .notification { position: relative; float: right; clear: both; margin: 10px; height: 50px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; perspective: 1000px; - background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px + background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; @@ -53,6 +53,9 @@ a { color: black } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } .notification-done .notification-icon { font-size: 22px; background-color: #27ae60 } +/* Notification input */ +.notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; border-radius: 1px; margin-right: -11px; transition: all 0.3s } +.notification .input:focus { border-color: #95a5a6; outline: none } /* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */ diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 01e91c31..4659caa6 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -40,7 +40,7 @@ a { color: black } .notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap } .notification { position: relative; float: right; clear: both; margin: 10px; height: 50px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; - background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px + background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; @@ -58,6 +58,9 @@ a { color: black } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } .notification-done .notification-icon { font-size: 22px; background-color: #27ae60 } +/* Notification input */ +.notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; -webkit-border-radius: 1px; -moz-border-radius: 1px; -o-border-radius: 1px; -ms-border-radius: 1px; border-radius: 1px ; margin-right: -11px; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } +.notification .input:focus { border-color: #95a5a6; outline: none } /* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */ diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 0d6ffc11..c4358dc3 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -107,7 +107,7 @@ }; ZeroWebsocket.prototype.onOpenWebsocket = function(e) { - this.log("Open", e); + this.log("Open"); if (this.onOpen != null) { return this.onOpen(e); } @@ -743,6 +743,11 @@ jQuery.extend( jQuery.easing, this.wrapperWsInited = false; this.site_error = null; window.onload = this.onLoad; + $(window).on("hashchange", function() { + var src; + src = $("#inner-iframe").attr("src").replace(/#.*/, "") + window.location.hash; + return $("#inner-iframe").attr("src", src); + }); this; } @@ -786,6 +791,8 @@ jQuery.extend( jQuery.easing, return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]); } else if (cmd === "wrapperConfirm") { return this.actionWrapperConfirm(message); + } else if (cmd === "wrapperPrompt") { + return this.actionWrapperPrompt(message); } else { return this.ws.send(message); } @@ -815,6 +822,44 @@ jQuery.extend( jQuery.easing, return this.notifications.add("notification-" + message.id, "ask", body); }; + Wrapper.prototype.actionWrapperPrompt = function(message) { + var body, button, caption, input, type; + message.params = this.toHtmlSafe(message.params); + if (message.params[1]) { + type = message.params[1]; + } else { + type = "text"; + } + caption = "OK"; + body = $("" + message.params[0] + ""); + input = $(""); + input.on("keyup", (function(_this) { + return function(e) { + if (e.keyCode === 13) { + return _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": input.val() + }); + } + }; + })(this)); + body.append(input); + button = $("" + caption + ""); + button.on("click", (function(_this) { + return function() { + _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": input.val() + }); + return false; + }; + })(this)); + body.append(button); + return this.notifications.add("notification-" + message.id, "ask", body); + }; + Wrapper.prototype.onOpenWebsocket = function(e) { this.ws.cmd("channelJoin", { "channel": "siteChanged" @@ -859,7 +904,7 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.onLoad = function(e) { - this.log("onLoad", e); + this.log("onLoad"); this.inner_loaded = true; if (!this.inner_ready) { this.sendInner({ @@ -869,6 +914,9 @@ jQuery.extend( jQuery.easing, if (!this.site_error) { this.loading.hideScreen(); } + if (window.location.hash) { + $("#inner-iframe")[0].src += window.location.hash; + } if (this.ws.ws.readyState === 1 && !this.site_info) { return this.reloadSiteInfo(); } diff --git a/src/Ui/media/lib/ZeroWebsocket.coffee b/src/Ui/media/lib/ZeroWebsocket.coffee index 4877d477..daa5228f 100644 --- a/src/Ui/media/lib/ZeroWebsocket.coffee +++ b/src/Ui/media/lib/ZeroWebsocket.coffee @@ -57,7 +57,7 @@ class ZeroWebsocket onOpenWebsocket: (e) => - @log "Open", e + @log "Open" if @onOpen? then @onOpen(e) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 7de9dac0..b267f82f 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -8,7 +8,7 @@ - +
@@ -35,8 +35,7 @@ - - +