Browse Source

version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status

HelloZeroNet 9 years ago
parent
commit
78f97dcbe8

+ 135 - 0
plugins/Stats/StatsPlugin.py

@@ -0,0 +1,135 @@
+import re, time, cgi, os
+from Plugin import PluginManager
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+	def formatTableRow(self, row):
+		back = []
+		for format, val in row:
+			if val == None: 
+				formatted = "n/a"
+			elif format == "since":
+				if val:
+					formatted = "%.0f" % (time.time()-val)
+				else:
+					formatted = "n/a"
+			else:
+				formatted = format % val
+			back.append("<td>%s</td>" % formatted)
+		return "<tr>%s</tr>" % "".join(back)
+
+
+	def getObjSize(self, obj, hpy = None):
+		if hpy:
+			return float(hpy.iso(obj).domisize)/1024
+		else:
+			return 0
+
+
+	# /Stats entry point
+	def actionStats(self):
+		import gc, sys
+		from Ui import UiRequest
+		
+		hpy = None
+		if self.get.get("size") == "1": # Calc obj size
+			try:
+				import guppy
+				hpy = guppy.hpy()
+			except:
+				pass
+		self.sendHeader()
+		s = time.time()
+		main = sys.modules["main"]
+
+		# Style
+		yield """
+		<style>
+		 * { font-family: monospace }
+		 table * { text-align: right; padding: 0px 10px }
+		</style>
+		"""
+
+		# Memory
+		try:
+			import psutil
+			process = psutil.Process(os.getpid())
+			mem = process.get_memory_info()[0] / float(2 ** 20)
+			yield "Memory usage: %.2fMB | " % mem
+			yield "Threads: %s | " % len(process.threads())
+			yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times()
+			yield "Open files: %s | " % len(process.open_files())
+			yield "Sockets: %s" % len(process.connections())
+			yield " | Calc size <a href='?size=1'>on</a> <a href='?size=0'>off</a><br>"
+		except Exception, err:
+			pass
+
+		yield "Connections (%s):<br>" % len(main.file_server.connections)
+		yield "<table><tr> <th>id</th> <th>protocol</th>  <th>type</th> <th>ip</th> <th>ping</th> <th>buff</th>"
+		yield "<th>idle</th> <th>open</th> <th>delay</th> <th>sent</th> <th>received</th> <th>last sent</th> <th>waiting</th> <th>version</th> <th>peerid</th> </tr>"
+		for connection in main.file_server.connections:
+			yield self.formatTableRow([
+				("%3d", connection.id),
+				("%s", connection.protocol),
+				("%s", connection.type),
+				("%s", connection.ip),
+				("%6.3f", connection.last_ping_delay),
+				("%s", connection.incomplete_buff_recv),
+				("since", max(connection.last_send_time, connection.last_recv_time)),
+				("since", connection.start_time),
+				("%.3f", connection.last_sent_time-connection.last_send_time),
+				("%.0fkB", connection.bytes_sent/1024),
+				("%.0fkB", connection.bytes_recv/1024),
+				("%s", connection.last_cmd),
+				("%s", connection.waiting_requests.keys()),
+				("%s", connection.handshake.get("version")),
+				("%s", connection.handshake.get("peer_id")),
+			])
+		yield "</table>"
+
+		from greenlet import greenlet
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
+		yield "<br>Greenlets (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+
+
+		from Worker import Worker
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)]
+		yield "<br>Workers (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+
+
+		from Connection import Connection
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)]
+		yield "<br>Connections (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+
+
+		from Site import Site
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)]
+		yield "<br>Sites (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+
+
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)]
+		yield "<br>Loggers (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj.name)))
+
+
+		objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)]
+		yield "<br>UiRequest (%s):<br>" % len(objs)
+		for obj in objs:
+			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+
+		objs = [(key, val) for key, val in sys.modules.iteritems() if val is not None]
+		objs.sort()
+		yield "<br>Modules (%s):<br>" % len(objs)
+		for module_name, module in objs:
+			yield " - %.3fkb: %s %s<br>" % (self.getObjSize(module, hpy), module_name, cgi.escape(repr(module)))
+
+		yield "Done in %.3f" % (time.time()-s)

+ 1 - 0
plugins/Stats/__init__.py

@@ -0,0 +1 @@
+import StatsPlugin

+ 24 - 0
plugins/disabled-DonationMessage/DonationMessagePlugin.py

@@ -0,0 +1,24 @@
+import re
+from Plugin import PluginManager
+
+# Warning: If you modify the donation address then renmae the plugin's directory to "MyDonationMessage" to prevent the update script overwrite
+
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+	# Inject a donation message to every page top right corner
+	def actionWrapper(self, path):
+		back = super(UiRequestPlugin, self).actionWrapper(path)
+		if not back or not hasattr(back, "endswith"): return back # Wrapper error or not string returned, injection not possible
+
+		back = re.sub("</body>\s*</html>\s*$", 
+			"""
+			<style>
+			 #donation_message { position: absolute; bottom: 0px; right: 20px; padding: 7px; font-family: Arial; font-size: 11px }
+			</style>
+			<a id='donation_message' href='https://blockchain.info/address/1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX' target='_blank'>Please donate to help to keep this ZeroProxy alive</a>
+			</body>
+			</html>
+			""", back)
+
+		return back

+ 1 - 0
plugins/disabled-DonationMessage/__init__.py

@@ -0,0 +1 @@
+import DonationMessagePlugin

+ 169 - 0
plugins/disabled-Multiuser/MultiuserPlugin.py

@@ -0,0 +1,169 @@
+import re, time, sys
+from Plugin import PluginManager
+from Crypt import CryptBitcoin
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+	def __init__(self, server = None):
+		self.user_manager = sys.modules["User.UserManager"].user_manager
+		super(UiRequestPlugin, self).__init__(server)
+
+
+	# Create new user and inject user welcome message if necessary
+	# Return: Html body also containing the injection
+	def actionWrapper(self, path):
+		user_created = False
+		user = self.getCurrentUser() # Get user from cookie
+
+		if not user: # No user found by cookie
+			user = self.user_manager.create()
+			user_created = True
+
+		master_address = user.master_address
+		master_seed = user.master_seed
+
+		if user_created: 
+			extra_headers = [('Set-Cookie', "master_address=%s;path=/;max-age=2592000;" % user.master_address)] # Max age = 30 days
+		else:
+			extra_headers = []
+
+		loggedin = self.get.get("login") == "done"
+
+		back = super(UiRequestPlugin, self).actionWrapper(path, extra_headers) # Get the wrapper frame output
+
+		if not user_created and not loggedin: return back # No injection necessary
+
+		if not back or not hasattr(back, "endswith"): return back # Wrapper error or not string returned, injection not possible
+
+		if user_created:
+			# Inject the welcome message
+			inject_html = """
+				<!-- Multiser plugin -->
+				<style>
+	 			 .masterseed { font-size: 95%; background-color: #FFF0AD; padding: 5px 8px; margin: 9px 0px }
+				</style>
+				<script>
+				 hello_message = "<b>Hello, welcome to ZeroProxy!</b><div style='margin-top: 8px'>A new, unique account created for you:</div>"
+				 hello_message+= "<div class='masterseed'>{master_seed}</div> <div>This is your private key, <b>save it</b>, so you can login next time.</div><br>"
+				 hello_message+= "<a href='#' class='button' style='margin-left: 0px'>Ok, Saved it!</a> or <a href='#Login' onclick='wrapper.ws.cmd(\\"userLoginForm\\", []); return false'>Login</a><br><br>"
+				 hello_message+= "<small>This site is allows you to browse ZeroNet content, but if you want to secure your account <br>"
+				 hello_message+= "and help to make a better network, then please run your own <a href='https://github.com/HelloZeroNet/ZeroNet' target='_blank'>ZeroNet client</a>.</small>"
+				 setTimeout(function() {
+				 	wrapper.notifications.add("hello", "info", hello_message)
+				 	delete(hello_message)
+				 }, 1000)
+				</script>
+				</body>
+				</html>
+			""".replace("\t", "")
+			inject_html = inject_html.replace("{master_seed}", master_seed) # Set the master seed in the message
+
+			back = re.sub("</body>\s*</html>\s*$", inject_html, back) # Replace the </body></html> tags with the injection 
+
+		elif loggedin:
+			inject_html = """
+				<!-- Multiser plugin -->
+				<script>
+				 setTimeout(function() {
+				 	wrapper.notifications.add("login", "done", "Hello again!<br><small>You have been logged in successfully</small>", 5000) 
+				 }, 1000)
+				</script>
+				</body>
+				</html>
+			""".replace("\t", "")
+			back = re.sub("</body>\s*</html>\s*$", inject_html, back) # Replace the </body></html> tags with the injection
+
+		return back 
+
+
+	# Get the current user based on request's cookies
+	# Return: User object or None if no match
+	def getCurrentUser(self):
+		cookies = self.getCookies()
+		user_manager = self.user_manager
+		user = None
+		if "master_address" in cookies:
+			users = self.user_manager.list()
+			user = users.get(cookies["master_address"])
+		return user
+
+
+@PluginManager.registerTo("UserManager")
+class UserManagerPlugin(object):
+	# In multiuser mode do not load the users
+	def load(self):
+		if not self.users: self.users = {}
+		return self.users
+
+
+	# Find user by master address
+	# Return: User or None
+	def get(self, master_address=None):
+		users = self.list()
+		if master_address in users:
+			user = users[master_address]
+		else:
+			user = None
+		return user
+
+
+@PluginManager.registerTo("User")
+class UserPlugin(object):
+	# In multiuser mode users data only exits in memory, dont write to data/user.json
+	def save(self):
+		return False 
+ 
+
+@PluginManager.registerTo("UiWebsocket")
+class UiWebsocketPlugin(object):
+	# Let the page know we running in multiuser mode
+	def formatServerInfo(self):
+		server_info = super(UiWebsocketPlugin, self).formatServerInfo()
+		server_info["multiuser"] = True
+		if "ADMIN" in self.site.settings["permissions"]:
+			server_info["master_address"] = self.user.master_address
+		return server_info
+
+
+	# Show current user's master seed
+	def actionUserShowMasterSeed(self, to):
+		if not "ADMIN" in self.site.settings["permissions"]: return self.response(to, "Show master seed not allowed")
+		message = "<b style='padding-top: 5px; display: inline-block'>Your unique private key:</b>"
+		message+= "<div style='font-size: 84%%; background-color: #FFF0AD; padding: 5px 8px; margin: 9px 0px'>%s</div>" % self.user.master_seed
+		message+= "<small>(Save it, you can access your account using this information)</small>"
+		self.cmd("notification", ["info", message])
+
+
+	# Logout user
+	def actionUserLogout(self, to):
+		if not "ADMIN" in self.site.settings["permissions"]: return self.response(to, "Logout not allowed")
+		message = "<b>You have been logged out.</b> <a href='#Login' class='button' onclick='wrapper.ws.cmd(\"userLoginForm\", []); return false'>Login to another account</a>"
+		message+= "<script>document.cookie = 'master_address=; expires=Thu, 01 Jan 1970 00:00:00 UTC'</script>"
+		self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :)
+		# Delete from user_manager
+		user_manager = sys.modules["User.UserManager"].user_manager
+		if self.user.master_address in user_manager.users:
+			del user_manager.users[self.user.master_address]
+			self.response(to, "Successful logout")
+		else:
+			self.response(to, "User not found")
+
+
+	# Show login form
+	def actionUserLoginForm(self, to):
+		self.cmd("prompt", ["<b>Login</b><br>Your private key:", "password", "Login"], self.responseUserLogin)
+
+
+	# Login form submit
+	def responseUserLogin(self, master_seed):
+		user_manager = sys.modules["User.UserManager"].user_manager
+		user = user_manager.create(master_seed=master_seed)
+		if user.master_address:
+			message = "Successfull login, reloading page..."
+			message+= "<script>document.cookie = 'master_address=%s;path=/;max-age=2592000;'</script>" % user.master_address
+			message+= "<script>wrapper.reload('login=done')</script>"
+			self.cmd("notification", ["done", message])
+		else:
+			self.cmd("notification", ["error", "Error: Invalid master seed"])
+			self.actionUserLoginForm(0)
+

+ 1 - 0
plugins/disabled-Multiuser/__init__.py

@@ -0,0 +1 @@
+import MultiuserPlugin

+ 1 - 1
src/Config.py

@@ -3,7 +3,7 @@ import ConfigParser
 
 class Config(object):
 	def __init__(self):
-		self.version = "0.2.6"
+		self.version = "0.2.7"
 		self.parser = self.createArguments()
 		argv = sys.argv[:] # Copy command line arguments
 		argv = self.parseConfig(argv) # Add arguments from config file

+ 2 - 2
src/Crypt/CryptBitcoin.py

@@ -1,5 +1,5 @@
-from src.lib.BitcoinECC import BitcoinECC
-from src.lib.pybitcointools import bitcoin as btctools
+from lib.BitcoinECC import BitcoinECC
+from lib.pybitcointools import bitcoin as btctools
 
 
 def newPrivatekey(uncompressed=True): # Return new private key

+ 2 - 0
src/Debug/DebugReloader.py

@@ -6,6 +6,7 @@ if config.debug: # Only load pyfilesytem if using debug mode
 	try:
 		from fs.osfs import OSFS 
 		pyfilesystem = OSFS("src")
+		pyfilesystem_plugins = OSFS("plugins")
 	except Exception, err:
 		logging.debug("%s: For autoreload please download pyfilesystem (https://code.google.com/p/pyfilesystem/)" % err)
 		pyfilesystem = False
@@ -28,6 +29,7 @@ class DebugReloader:
 		try:
 			time.sleep(1) # Wait for .pyc compiles
 			pyfilesystem.add_watcher(self.changed, path=self.directory, events=None, recursive=recursive)
+			pyfilesystem_plugins.add_watcher(self.changed, path=self.directory, events=None, recursive=recursive)
 		except Exception, err:
 			print "File system watcher failed: %s (on linux pyinotify not gevent compatible yet :( )" % err
 

+ 1 - 1
src/File/FileServer.py

@@ -1,4 +1,4 @@
-import os, logging, urllib2, urllib, re, time
+import os, logging, urllib2, re, time
 import gevent, msgpack
 import zmq.green as zmq
 from Config import config

+ 1 - 1
src/Peer/Peer.py

@@ -12,7 +12,7 @@ class Peer:
 		self.site = site
 		self.key = "%s:%s" % (ip, port)
 		self.log = None
-		self.connection_server = sys.modules["src.main"].file_server
+		self.connection_server = sys.modules["main"].file_server
 
 		self.connection = None
 		self.last_found = None # Time of last found in the torrent tracker

+ 97 - 0
src/Plugin/PluginManager.py

@@ -0,0 +1,97 @@
+import logging, os, sys
+from Debug import Debug
+from Config import config
+
+class PluginManager:
+	def __init__(self):
+		self.log = logging.getLogger("PluginManager")
+		self.plugin_path = "plugins" # Plugin directory
+		self.plugins = {} # Registered plugins (key: class name, value: list of plugins for class)
+
+		sys.path.append(self.plugin_path)
+
+
+		if config.debug: # Auto reload Plugins on file change
+			from Debug import DebugReloader
+			DebugReloader(self.reloadPlugins)
+
+
+	# -- Load / Unload --
+
+	# Load all plugin
+	def loadPlugins(self):
+		for dir_name in os.listdir(self.plugin_path):
+			dir_path = os.path.join(self.plugin_path, dir_name)
+			if dir_name.startswith("disabled"): continue # Dont load if disabled
+			if not os.path.isdir(dir_path): continue # Dont load if not dir
+			if dir_name.startswith("Debug") and not config.debug: continue # Only load in debug mode if module name starts with Debug
+			self.log.debug("Loading plugin: %s" % dir_name)
+			try:
+				__import__(dir_name)
+			except Exception, err:
+				self.log.error("Plugin %s load error: %s" % (dir_name, Debug.formatException(err)))
+
+
+	# Reload all plugins
+	def reloadPlugins(self):
+		self.plugins = {} # Reset registered plugins
+		for module_name, module in sys.modules.items():
+			if module and "__file__" in dir(module) and self.plugin_path in module.__file__: # Module file within plugin_path
+				if "allow_reload" not in dir(module) or module.allow_reload: # Check if reload disabled
+					try:
+						reload(module)
+					except Exception, err:
+						self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err)))
+
+		self.loadPlugins() # Load new plugins
+
+
+plugin_manager = PluginManager() # Singletone
+
+# -- Decorators --
+
+# Accept plugin to class decorator
+def acceptPlugins(base_class):
+	class_name = base_class.__name__
+	if class_name in plugin_manager.plugins: # Has plugins
+		classes = plugin_manager.plugins[class_name][:] # Copy the current plugins
+		classes.reverse()
+		classes.append(base_class) # Add the class itself to end of inherience line
+		PluginedClass = type(class_name, tuple(classes), dict()) # Create the plugined class
+		plugin_manager.log.debug("New class accepts plugins: %s (Loaded plugins: %s)" % (class_name, classes))
+	else: # No plugins just use the original
+		PluginedClass = base_class
+	return PluginedClass
+
+
+# Register plugin to class name decorator
+def registerTo(class_name):
+	plugin_manager.log.debug("New plugin registered to: %s" % class_name)
+	if class_name not in plugin_manager.plugins: plugin_manager.plugins[class_name] = []
+
+	def classDecorator(self):
+		plugin_manager.plugins[class_name].append(self)
+		return self
+	return classDecorator
+
+
+
+# - Example usage -
+
+if __name__ == "__main__":
+	@registerTo("Request")
+	class RequestPlugin(object):
+		def actionMainPage(self, path):
+			return "Hello MainPage!"
+
+
+	@accept
+	class Request(object):
+		def route(self, path):
+			func = getattr(self, "action"+path, None)
+			if func:
+				return func(path)
+			else:
+				return "Can't route to", path
+
+	print Request().route("MainPage")

+ 0 - 0
src/Plugin/__init__.py


+ 49 - 146
src/Ui/UiRequest.py

@@ -2,6 +2,7 @@ import time, re, os, mimetypes, json, cgi
 from Config import config
 from Site import SiteManager
 from User import UserManager
+from Plugin import PluginManager
 from Ui.UiWebsocket import UiWebsocket
 
 status_texts = {
@@ -12,15 +13,15 @@ status_texts = {
 }
 
 
-
-class UiRequest:
+@PluginManager.acceptPlugins
+class UiRequest(object):
 	def __init__(self, server = None):
 		if server:
 			self.server = server
 			self.log = server.log
 		self.get = {} # Get parameters
 		self.env = {} # Enviroment settings
-		self.user = UserManager.getCurrent()
+		self.user = None
 		self.start_response = None # Start response function
 
 
@@ -46,16 +47,17 @@ class UiRequest:
 			return self.actionDebug()
 		elif path == "/Console" and config.debug:
 			return self.actionConsole()
-		elif path == "/Stats":
-			return self.actionStats()
-		# Test
-		elif path == "/Test/Websocket":
-			return self.actionFile("Data/temp/ws_test.html")
-		elif path == "/Test/Stream":
-			return self.actionTestStream()
 		# Site media wrapper
 		else:
-			return self.actionWrapper(path)
+			body = self.actionWrapper(path)
+			if body:
+				return body
+			else:
+				func = getattr(self, "action"+path.lstrip("/"), None) # Check if we have action+request_path function
+				if func:
+					return func()
+				else:
+					return self.error404(path)
 
 
 	# Get mime by filename
@@ -69,6 +71,24 @@ class UiRequest:
 		return content_type
 
 
+	# Returns: <dict> Cookies based on self.env
+	def getCookies(self):
+		raw_cookies = self.env.get('HTTP_COOKIE')
+		if raw_cookies:
+			cookies = cgi.parse_qsl(raw_cookies)
+			return {key.strip(): val for key, val in cookies}
+		else:
+			return {}
+
+
+	def getCurrentUser(self):
+		if self.user: return self.user # Cache
+		self.user = UserManager.user_manager.get() # Get user
+		if not self.user:
+			self.user = UserManager.user_manager.create()
+		return self.user
+
+
 	# Send response headers
 	def sendHeader(self, status=200, content_type="text/html", extra_headers=[]):
 		if content_type == "text/html": content_type = "text/html; charset=utf-8"
@@ -89,7 +109,7 @@ class UiRequest:
 		#template = SimpleTemplate(open(template_path), lookup=[os.path.dirname(template_path)])
 		#yield str(template.render(*args, **kwargs).encode("utf8"))
 		template = open(template_path).read().decode("utf8")
-		yield template.format(**kwargs).encode("utf8")
+		return template.format(**kwargs).encode("utf8")
 
 
 	# - Actions -
@@ -105,7 +125,7 @@ class UiRequest:
 
 
 	# Render a file from media with iframe site wrapper
-	def actionWrapper(self, path):
+	def actionWrapper(self, path, extra_headers=[]):
 		if "." in path and not path.endswith(".html"): return self.actionSiteMedia("/media"+path) # Only serve html files with frame
 		if self.get.get("wrapper") == "False": 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
@@ -121,9 +141,11 @@ class UiRequest:
 			else:
 				title = "Loading %s..." % match.group("site")
 				site = SiteManager.need(match.group("site")) # Start download site
-				if not site: return self.error404(path)
+				if not site: return False
+
+			extra_headers.append(("X-Frame-Options", "DENY"))
 
-			self.sendHeader(extra_headers=[("X-Frame-Options", "DENY")])
+			self.sendHeader(extra_headers=extra_headers)
 
 			# Wrapper variable inits
 			query_string = ""
@@ -152,7 +174,7 @@ class UiRequest:
 			)
 
 		else: # Bad url
-			return self.error404(path)
+			return False
 
 
 	# Serve a media for site
@@ -241,7 +263,11 @@ class UiRequest:
 				if site_check.settings["wrapper_key"] == wrapper_key: site = site_check
 
 			if site: # Correct wrapper key
-				ui_websocket = UiWebsocket(ws, site, self.server, self.user)
+				user = self.getCurrentUser()
+				if not user:
+					self.log.error("No user found")
+					return self.error403()
+				ui_websocket = UiWebsocket(ws, site, self.server, user)
 				site.websockets.append(ui_websocket) # Add to site websockets to allow notify on events
 				ui_websocket.start()
 				for site_check in self.server.sites.values(): # Remove websocket from every site (admin sites allowed to join other sites event channels)
@@ -260,7 +286,7 @@ class UiRequest:
 	def actionDebug(self):
 		# Raise last error from DebugHook
 		import sys
-		last_error = sys.modules["src.main"].DebugHook.last_error
+		last_error = sys.modules["main"].DebugHook.last_error
 		if last_error:
 			raise last_error[0], last_error[1], last_error[2]
 		else:
@@ -272,134 +298,10 @@ class UiRequest:
 	def actionConsole(self):
 		import sys
 		sites = self.server.sites
-		main = sys.modules["src.main"]
+		main = sys.modules["main"]
 		raise Exception("Here is your console")
 
 
-	def formatTableRow(self, row):
-		back = []
-		for format, val in row:
-			if val == None: 
-				formatted = "n/a"
-			elif format == "since":
-				if val:
-					formatted = "%.0f" % (time.time()-val)
-				else:
-					formatted = "n/a"
-			else:
-				formatted = format % val
-			back.append("<td>%s</td>" % formatted)
-		return "<tr>%s</tr>" % "".join(back)
-
-
-	def getObjSize(self, obj, hpy = None):
-		if hpy:
-			return float(hpy.iso(obj).domisize)/1024
-		else:
-			return 0
-
-
-
-	def actionStats(self):
-		import gc, sys
-		hpy = None
-		if self.get.get("size") == "1": # Calc obj size
-			try:
-				import guppy
-				hpy = guppy.hpy()
-			except:
-				pass
-		self.sendHeader()
-		s = time.time()
-		main = sys.modules["src.main"]
-
-		# Style
-		yield """
-		<style>
-		 * { font-family: monospace }
-		 table * { text-align: right; padding: 0px 10px }
-		</style>
-		"""
-
-		# Memory
-		try:
-			import psutil
-			process = psutil.Process(os.getpid())
-			mem = process.get_memory_info()[0] / float(2 ** 20)
-			yield "Memory usage: %.2fMB | " % mem
-			yield "Threads: %s | " % len(process.threads())
-			yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times()
-			yield "Open files: %s | " % len(process.open_files())
-			yield "Sockets: %s" % len(process.connections())
-			yield " | Calc size <a href='?size=1'>on</a> <a href='?size=0'>off</a><br>"
-		except Exception, err:
-			pass
-
-		yield "Connections (%s):<br>" % len(main.file_server.connections)
-		yield "<table><tr> <th>id</th> <th>protocol</th>  <th>type</th> <th>ip</th> <th>ping</th> <th>buff</th>"
-		yield "<th>idle</th> <th>open</th> <th>delay</th> <th>sent</th> <th>received</th> <th>last sent</th> <th>waiting</th> <th>version</th> <th>peerid</th> </tr>"
-		for connection in main.file_server.connections:
-			yield self.formatTableRow([
-				("%3d", connection.id),
-				("%s", connection.protocol),
-				("%s", connection.type),
-				("%s", connection.ip),
-				("%6.3f", connection.last_ping_delay),
-				("%s", connection.incomplete_buff_recv),
-				("since", max(connection.last_send_time, connection.last_recv_time)),
-				("since", connection.start_time),
-				("%.3f", connection.last_sent_time-connection.last_send_time),
-				("%.0fkB", connection.bytes_sent/1024),
-				("%.0fkB", connection.bytes_recv/1024),
-				("%s", connection.last_cmd),
-				("%s", connection.waiting_requests.keys()),
-				("%s", connection.handshake.get("version")),
-				("%s", connection.handshake.get("peer_id")),
-			])
-		yield "</table>"
-
-		from greenlet import greenlet
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
-		yield "<br>Greenlets (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
-
-
-		from Worker import Worker
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)]
-		yield "<br>Workers (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
-
-
-		from Connection import Connection
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)]
-		yield "<br>Connections (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
-
-
-		from Site import Site
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)]
-		yield "<br>Sites (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
-
-
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)]
-		yield "<br>Loggers (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj.name)))
-
-
-		objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)]
-		yield "<br>UiRequest (%s):<br>" % len(objs)
-		for obj in objs:
-			yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
-
-		yield "Done in %.3f" % (time.time()-s)
-
-
 	# - Tests -
 
 	def actionTestStream(self):
@@ -433,8 +335,9 @@ class UiRequest:
 
 	# - Reload for eaiser developing -
 	def reload(self):
-		import imp
+		import imp, sys
 		global UiWebsocket
 		UiWebsocket = imp.load_source("UiWebsocket", "src/Ui/UiWebsocket.py").UiWebsocket
-		UserManager.reload()
-		self.user = UserManager.getCurrent()
+		#reload(sys.modules["User.UserManager"])
+		#UserManager.reloadModule()
+		#self.user = UserManager.user_manager.getCurrent()

+ 8 - 3
src/Ui/UiServer.py

@@ -3,11 +3,12 @@ import logging, time, cgi, string, random
 from gevent.pywsgi import WSGIServer
 from gevent.pywsgi import WSGIHandler
 from lib.geventwebsocket.handler import WebSocketHandler
-from Ui import UiRequest
+from UiRequest import UiRequest
 from Site import SiteManager
 from Config import config
 from Debug import Debug
 
+
 # Skip websocket handler if not necessary
 class UiWSGIHandler(WSGIHandler):
 	def __init__(self, *args, **kwargs):
@@ -28,7 +29,10 @@ class UiWSGIHandler(WSGIHandler):
 			try:
 				return super(UiWSGIHandler, self).run_application()
 			except Exception, err:
-				logging.debug("UiWSGIHandler error: %s" % err)
+				logging.debug("UiWSGIHandler error: %s" % Debug.formatException(err))
+				if config.debug: # Allow websocket errors to appear on /Debug 
+					import sys
+					sys.modules["main"].DebugHook.handleError() 
 		del self.server.sockets[self.client_address]
 
 
@@ -59,7 +63,8 @@ class UiServer:
 
 	# Reload the UiRequest class to prevent restarts in debug mode
 	def reload(self):
-		import imp
+		import imp, sys
+		reload(sys.modules["User.UserManager"])
 		self.ui_request = imp.load_source("UiRequest", "src/Ui/UiRequest.py").UiRequest(self)
 		self.ui_request.reload()
 

+ 23 - 16
src/Ui/UiWebsocket.py

@@ -3,9 +3,10 @@ from Config import config
 from Site import SiteManager
 from Debug import Debug
 from util import QueryJson
+from Plugin import PluginManager
 
-
-class UiWebsocket:
+@PluginManager.acceptPlugins
+class UiWebsocket(object):
 	def __init__(self, ws, site, server, user):
 		self.ws = ws
 		self.site = site
@@ -41,7 +42,7 @@ class UiWebsocket:
 				if err.message != 'Connection is already closed':
 					if config.debug: # Allow websocket errors to appear on /Debug 
 						import sys
-						sys.modules["src.main"].DebugHook.handleError() 
+						sys.modules["main"].DebugHook.handleError() 
 					self.log.error("WebSocket error: %s" % Debug.formatException(err))
 				return "Bye."
 
@@ -133,10 +134,12 @@ class UiWebsocket:
 			func = self.actionChannelJoinAllsite
 		elif cmd == "serverUpdate" and "ADMIN" in permissions:
 			func = self.actionServerUpdate
-		# Unknown command
 		else:
-			self.response(req["id"], "Unknown command: %s" % cmd)
-			return
+			func_name = "action" + cmd[0].upper() + cmd[1:]
+			func = getattr(self, func_name, None)
+			if not func: # Unknown command
+				self.response(req["id"], "Unknown command: %s" % cmd)
+				return
 
 		# Support calling as named, unnamed paramters and raw first argument too
 		if type(params) is dict:
@@ -152,7 +155,7 @@ class UiWebsocket:
 	# Do callback on response {"cmd": "response", "to": message_id, "result": result}
 	def actionResponse(self, to, result):
 		if to in self.waiting_cb:
-			self.waiting_cb(result) # Call callback function
+			self.waiting_cb[to](result) # Call callback function
 		else:
 			self.log.error("Websocket callback not found: %s, %s" % (to, result))
 
@@ -163,7 +166,7 @@ class UiWebsocket:
 
 
 	# Format site info
-	def formatSiteInfo(self, site):
+	def formatSiteInfo(self, site, create_user=True):
 		content = site.content_manager.contents.get("content.json")
 		if content: # Remove unnecessary data transfer
 			content = content.copy()
@@ -179,7 +182,7 @@ class UiWebsocket:
 		ret = {
 			"auth_key": self.site.settings["auth_key"], # Obsolete, will be removed
 			"auth_key_sha512": hashlib.sha512(self.site.settings["auth_key"]).hexdigest()[0:64], # Obsolete, will be removed
-			"auth_address": self.user.getAuthAddress(site.address),
+			"auth_address": self.user.getAuthAddress(site.address, create=create_user),
 			"address": site.address,
 			"settings": settings,
 			"content_updated": site.content_updated,
@@ -208,9 +211,8 @@ class UiWebsocket:
 			self.channels.append(channel)
 
 
-	# Server variables
-	def actionServerInfo(self, to):
-		ret = {
+	def formatServerInfo(self):
+		return {
 			"ip_external": bool(config.ip_external),
 			"platform": sys.platform,
 			"fileserver_ip": config.fileserver_ip,
@@ -220,6 +222,11 @@ class UiWebsocket:
 			"version": config.version,
 			"debug": config.debug
 		}
+
+
+	# Server variables
+	def actionServerInfo(self, to):
+		ret = self.formatServerInfo()
 		self.response(to, ret)
 
 
@@ -323,7 +330,7 @@ class UiWebsocket:
 		SiteManager.load() # Reload sites
 		for site in self.server.sites.values():
 			if not site.content_manager.contents.get("content.json"): continue # Broken site
-			ret.append(self.formatSiteInfo(site))
+			ret.append(self.formatSiteInfo(site, create_user=False))
 		self.response(to, ret)
 
 
@@ -395,7 +402,7 @@ class UiWebsocket:
 	def actionServerUpdate(self, to):
 		import sys
 		self.cmd("updating")
-		sys.modules["src.main"].update_after_shutdown = True
-		sys.modules["src.main"].file_server.stop()
-		sys.modules["src.main"].ui_server.stop()
+		sys.modules["main"].update_after_shutdown = True
+		sys.modules["main"].file_server.stop()
+		sys.modules["main"].ui_server.stop()
 

+ 3 - 1
src/Ui/media/Notifications.coffee

@@ -33,7 +33,7 @@ class Notifications
 			$(".notification-icon", elem).html("i")
 
 		if typeof(body) == "string"
-			$(".body", elem).html(body)
+			$(".body", elem).html("<span class='message'>"+body+"</span>")
 		else
 			$(".body", elem).html("").append(body)
 
@@ -49,9 +49,11 @@ class Notifications
 		# Animate
 		width = elem.outerWidth()
 		if not timeout then width += 20 # Add space for close button
+		if elem.outerHeight() > 55 then elem.addClass("long")
 		elem.css({"width": "50px", "transform": "scale(0.01)"})
 		elem.animate({"scale": 1}, 800, "easeOutElastic")
 		elem.animate({"width": width}, 700, "easeInOutCubic")
+		$(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000)
 
 		# Close button
 		$(".close", elem).on "click", =>

+ 45 - 19
src/Ui/media/Wrapper.coffee

@@ -42,6 +42,9 @@ class Wrapper
 				@sendInner message # Pass message to inner frame
 		else if cmd == "notification" # Display notification
 			@notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2])
+		else if cmd == "prompt" # Prompt input
+			@displayPrompt message.params[0], message.params[1], message.params[2], (res) =>
+				@ws.response message.id, res
 		else if cmd == "setSiteInfo"
 			@sendInner message # Pass to inner frame
 			if message.params.address == window.address # Current page
@@ -63,18 +66,19 @@ class Wrapper
 				@sendInner {"cmd": "wrapperOpenedWebsocket"}
 				@wrapperWsInited = true
 		else if cmd == "wrapperNotification" # Display notification
-			message.params = @toHtmlSafe(message.params) # Escape html
-			@notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2])
+			@actionNotification(message)
 		else if cmd == "wrapperConfirm" # Display confirm message
-			@actionWrapperConfirm(message)
+			@actionConfirm(message)
 		else if cmd == "wrapperPrompt" # Prompt input
-			@actionWrapperPrompt(message)
+			@actionPrompt(message)
 		else if cmd == "wrapperSetViewport" # Set the viewport
 			@actionSetViewport(message)
+		else if cmd == "wrapperReload" # Reload current page
+			@actionReload(message)
 		else if cmd == "wrapperGetLocalStorage"
 			@actionGetLocalStorage(message)
 		else if cmd == "wrapperSetLocalStorage"
-			@actionSetLocalStorage(message)			
+			@actionSetLocalStorage(message)
 		else # Send to websocket
 			if message.id < 1000000
 				@ws.send(message) # Pass message to websocket
@@ -84,46 +88,58 @@ class Wrapper
 
 	# - Actions -
 
-	actionWrapperConfirm: (message, cb=false) ->
+	actionNotification: (message) ->
 		message.params = @toHtmlSafe(message.params) # Escape html
-		if message.params[1] then caption = message.params[1] else caption = "ok"
-		@wrapperConfirm message.params[0], caption, =>
-			@sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm
-			return false
+		body =  $("<span class='message'>"+message.params[1]+"</span>")
+		@notifications.add("notification-#{message.id}", message.params[0], body, message.params[2])
+
 
 
-	wrapperConfirm: (message, caption, cb) ->
-		body = $("<span>"+message+"</span>")
+	displayConfirm: (message, caption, cb) ->
+		body = $("<span class='message'>"+message+"</span>")
 		button = $("<a href='##{caption}' class='button button-#{caption}'>#{caption}</a>") # Add confirm button
 		button.on "click", cb
 		body.append(button)
 		@notifications.add("notification-#{caption}", "ask", body)
 
 
-
-	actionWrapperPrompt: (message) ->
+	actionConfirm: (message, cb=false) ->
 		message.params = @toHtmlSafe(message.params) # Escape html
-		if message.params[1] then type = message.params[1] else type = "text"
-		caption = "OK"
+		if message.params[1] then caption = message.params[1] else caption = "ok"
+		@displayConfirm message.params[0], caption, =>
+			@sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm
+			return false
+
 
-		body = $("<span>"+message.params[0]+"</span>")
+
+	displayPrompt: (message, type, caption, cb) ->
+		body = $("<span class='message'>"+message+"</span>")
 
 		input = $("<input type='#{type}' class='input button-#{type}'/>") # Add input
 		input.on "keyup", (e) => # Send on enter
 			if e.keyCode == 13
 				button.trigger "click" # Response to confirm
-
 		body.append(input)
 
 		button = $("<a href='##{caption}' class='button button-#{caption}'>#{caption}</a>") # Add confirm button
 		button.on "click", => # Response on button click
-			@sendInner {"cmd": "response", "to": message.id, "result": input.val()} # Response to confirm
+			cb input.val()
 			return false
 		body.append(button)
 
 		@notifications.add("notification-#{message.id}", "ask", body)
 
 
+	actionPrompt: (message) ->
+		message.params = @toHtmlSafe(message.params) # Escape html
+		if message.params[1] then type = message.params[1] else type = "text"
+		caption = "OK"
+		
+		@displayPrompt message.params[0], type, caption, (res) =>
+			@sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm
+		
+
+
 	actionSetViewport: (message) ->
 		@log "actionSetViewport", message
 		if $("#viewport").length > 0
@@ -132,6 +148,16 @@ class Wrapper
 			$('<meta name="viewport" id="viewport">').attr("content", @toHtmlSafe message.params).appendTo("head")
 
 
+	reload: (url_post="") ->
+		if url_post
+			if window.location.toString().indexOf("?") > 0
+				window.location += "&"+url_post
+			else
+				window.location += "?"+url_post
+		else
+			window.location.reload()
+
+
 	actionGetLocalStorage: (message) ->
 		data = localStorage.getItem "site.#{window.address}"
 		if data then data = JSON.parse(data)

+ 11 - 4
src/Ui/media/Wrapper.css

@@ -33,21 +33,28 @@ a { color: black }
 
 /* Notification */
 
-.notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap }
+.notifications { position: absolute; top: 0px; right: 80px; 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; /*border: 1px solid rgba(210, 206, 205, 0.2)*/
+	position: relative; float: right; clear: both; margin: 10px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; perspective: 1000px; padding-bottom: 5px;
+	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;
 	text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white;
 }
-.notification .body { max-width: 420px; padding-left: 68px; padding-right: 17px; height: 50px; vertical-align: middle; display: table-cell }
+.notification .body { 
+	max-width: 420px; padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table;
+	background-color: white; left: 50px; top: 0px; position: relative; padding-top: 5px; padding-bottom: 5px;
+}
+.notification.long .body { padding-top: 10px; padding-bottom: 10px }
+.notification .message { display: table-cell; vertical-align: middle }
+
 .notification.visible { max-width: 350px }
 
 .notification .close { position: absolute; top: 0px; right: 0px; font-size: 19px; line-height: 13px; color: #DDD; padding: 7px; text-decoration: none }
 .notification .close:hover { color: black }
 .notification .close:active, .notification .close:focus { color: #AF3BFF }
+.notification small { color: #AAA }
 .body-white .notification { box-shadow: 0px 1px 9px rgba(0,0,0,0.1) }
 
 /* Notification types */

+ 11 - 4
src/Ui/media/all.css

@@ -38,21 +38,28 @@ a { color: black }
 
 /* Notification */
 
-.notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap }
+.notifications { position: absolute; top: 0px; right: 80px; 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; /*border: 1px solid rgba(210, 206, 205, 0.2)*/
+	position: relative; float: right; clear: both; margin: 10px; -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 ; padding-bottom: 5px;
+	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;
 	text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white;
 }
-.notification .body { max-width: 420px; padding-left: 68px; padding-right: 17px; height: 50px; vertical-align: middle; display: table-cell }
+.notification .body { 
+	max-width: 420px; padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table;
+	background-color: white; left: 50px; top: 0px; position: relative; padding-top: 5px; padding-bottom: 5px;
+}
+.notification.long .body { padding-top: 10px; padding-bottom: 10px }
+.notification .message { display: table-cell; vertical-align: middle }
+
 .notification.visible { max-width: 350px }
 
 .notification .close { position: absolute; top: 0px; right: 0px; font-size: 19px; line-height: 13px; color: #DDD; padding: 7px; text-decoration: none }
 .notification .close:hover { color: black }
 .notification .close:active, .notification .close:focus { color: #AF3BFF }
+.notification small { color: #AAA }
 .body-white .notification { -webkit-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -moz-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -o-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -ms-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; box-shadow: 0px 1px 9px rgba(0,0,0,0.1)  }
 
 /* Notification types */

+ 74 - 36
src/Ui/media/all.js

@@ -149,7 +149,6 @@
 }).call(this);
 
 
-
 /* ---- src/Ui/media/lib/jquery.cssanim.js ---- */
 
 
@@ -247,7 +246,6 @@ jQuery.fx.step.scale = function(fx) {
 }).call(this);
 
 
-
 /* ---- src/Ui/media/lib/jquery.easing.1.3.js ---- */
 
 
@@ -542,7 +540,6 @@ jQuery.extend( jQuery.easing,
 }).call(this);
 
 
-
 /* ---- src/Ui/media/Notifications.coffee ---- */
 
 
@@ -593,7 +590,7 @@ jQuery.extend( jQuery.easing,
         $(".notification-icon", elem).html("i");
       }
       if (typeof body === "string") {
-        $(".body", elem).html(body);
+        $(".body", elem).html("<span class='message'>" + body + "</span>");
       } else {
         $(".body", elem).html("").append(body);
       }
@@ -610,6 +607,9 @@ jQuery.extend( jQuery.easing,
       if (!timeout) {
         width += 20;
       }
+      if (elem.outerHeight() > 55) {
+        elem.addClass("long");
+      }
       elem.css({
         "width": "50px",
         "transform": "scale(0.01)"
@@ -620,6 +620,7 @@ jQuery.extend( jQuery.easing,
       elem.animate({
         "width": width
       }, 700, "easeInOutCubic");
+      $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000);
       $(".close", elem).on("click", (function(_this) {
         return function() {
           _this.close(elem);
@@ -725,7 +726,6 @@ jQuery.extend( jQuery.easing,
 }).call(this);
 
 
-
 /* ---- src/Ui/media/Wrapper.coffee ---- */
 
 
@@ -786,6 +786,12 @@ jQuery.extend( jQuery.easing,
         }
       } else if (cmd === "notification") {
         return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]);
+      } else if (cmd === "prompt") {
+        return this.displayPrompt(message.params[0], message.params[1], message.params[2], (function(_this) {
+          return function(res) {
+            return _this.ws.response(message.id, res);
+          };
+        })(this));
       } else if (cmd === "setSiteInfo") {
         this.sendInner(message);
         if (message.params.address === window.address) {
@@ -812,14 +818,15 @@ jQuery.extend( jQuery.easing,
           return this.wrapperWsInited = true;
         }
       } else if (cmd === "wrapperNotification") {
-        message.params = this.toHtmlSafe(message.params);
-        return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]);
+        return this.actionNotification(message);
       } else if (cmd === "wrapperConfirm") {
-        return this.actionWrapperConfirm(message);
+        return this.actionConfirm(message);
       } else if (cmd === "wrapperPrompt") {
-        return this.actionWrapperPrompt(message);
+        return this.actionPrompt(message);
       } else if (cmd === "wrapperSetViewport") {
         return this.actionSetViewport(message);
+      } else if (cmd === "wrapperReload") {
+        return this.actionReload(message);
       } else if (cmd === "wrapperGetLocalStorage") {
         return this.actionGetLocalStorage(message);
       } else if (cmd === "wrapperSetLocalStorage") {
@@ -833,7 +840,23 @@ jQuery.extend( jQuery.easing,
       }
     };
 
-    Wrapper.prototype.actionWrapperConfirm = function(message, cb) {
+    Wrapper.prototype.actionNotification = function(message) {
+      var body;
+      message.params = this.toHtmlSafe(message.params);
+      body = $("<span class='message'>" + message.params[1] + "</span>");
+      return this.notifications.add("notification-" + message.id, message.params[0], body, message.params[2]);
+    };
+
+    Wrapper.prototype.displayConfirm = function(message, caption, cb) {
+      var body, button;
+      body = $("<span class='message'>" + message + "</span>");
+      button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
+      button.on("click", cb);
+      body.append(button);
+      return this.notifications.add("notification-" + caption, "ask", body);
+    };
+
+    Wrapper.prototype.actionConfirm = function(message, cb) {
       var caption;
       if (cb == null) {
         cb = false;
@@ -844,7 +867,7 @@ jQuery.extend( jQuery.easing,
       } else {
         caption = "ok";
       }
-      return this.wrapperConfirm(message.params[0], caption, (function(_this) {
+      return this.displayConfirm(message.params[0], caption, (function(_this) {
         return function() {
           _this.sendInner({
             "cmd": "response",
@@ -856,25 +879,9 @@ jQuery.extend( jQuery.easing,
       })(this));
     };
 
-    Wrapper.prototype.wrapperConfirm = function(message, caption, cb) {
-      var body, button;
-      body = $("<span>" + message + "</span>");
-      button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
-      button.on("click", cb);
-      body.append(button);
-      return this.notifications.add("notification-" + caption, "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 = $("<span>" + message.params[0] + "</span>");
+    Wrapper.prototype.displayPrompt = function(message, type, caption, cb) {
+      var body, button, input;
+      body = $("<span class='message'>" + message + "</span>");
       input = $("<input type='" + type + "' class='input button-" + type + "'/>");
       input.on("keyup", (function(_this) {
         return function(e) {
@@ -887,11 +894,7 @@ jQuery.extend( jQuery.easing,
       button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
       button.on("click", (function(_this) {
         return function() {
-          _this.sendInner({
-            "cmd": "response",
-            "to": message.id,
-            "result": input.val()
-          });
+          cb(input.val());
           return false;
         };
       })(this));
@@ -899,6 +902,26 @@ jQuery.extend( jQuery.easing,
       return this.notifications.add("notification-" + message.id, "ask", body);
     };
 
+    Wrapper.prototype.actionPrompt = function(message) {
+      var caption, type;
+      message.params = this.toHtmlSafe(message.params);
+      if (message.params[1]) {
+        type = message.params[1];
+      } else {
+        type = "text";
+      }
+      caption = "OK";
+      return this.displayPrompt(message.params[0], type, caption, (function(_this) {
+        return function(res) {
+          return _this.sendInner({
+            "cmd": "response",
+            "to": message.id,
+            "result": res
+          });
+        };
+      })(this));
+    };
+
     Wrapper.prototype.actionSetViewport = function(message) {
       this.log("actionSetViewport", message);
       if ($("#viewport").length > 0) {
@@ -908,6 +931,21 @@ jQuery.extend( jQuery.easing,
       }
     };
 
+    Wrapper.prototype.reload = function(url_post) {
+      if (url_post == null) {
+        url_post = "";
+      }
+      if (url_post) {
+        if (window.location.toString().indexOf("?") > 0) {
+          return window.location += "&" + url_post;
+        } else {
+          return window.location += "?" + url_post;
+        }
+      } else {
+        return window.location.reload();
+      }
+    };
+
     Wrapper.prototype.actionGetLocalStorage = function(message) {
       var data;
       data = localStorage.getItem("site." + window.address);
@@ -1116,4 +1154,4 @@ jQuery.extend( jQuery.easing,
 
   window.wrapper = new Wrapper(ws_url);
 
-}).call(this);
+}).call(this);

+ 14 - 10
src/User/User.py

@@ -1,9 +1,14 @@
 import logging, json, time
 from Crypt import CryptBitcoin
+from Plugin import PluginManager
 
-class User:
-	def __init__(self, master_address=None):
-		if master_address:
+@PluginManager.acceptPlugins
+class User(object):
+	def __init__(self, master_address=None, master_seed=None):
+		if master_seed:
+			self.master_seed = master_seed
+			self.master_address = CryptBitcoin.privatekeyToAddress(self.master_seed)
+		elif master_address:
 			self.master_address = master_address
 			self.master_seed = None
 		else:
@@ -27,8 +32,9 @@ class User:
 
 	# Get user site data
 	# Return: {"auth_address": "xxx", "auth_privatekey": "xxx"}
-	def getSiteData(self, address):
+	def getSiteData(self, address, create=True):
 		if not address in self.sites: # Genreate new BIP32 child key based on site address
+			if not create: return {"auth_address": None, "auth_privatekey": None} # Dont create user yet
 			s = time.time()
 			address_id = int(address.encode("hex"), 16) # Convert site address to int
 			auth_privatekey = CryptBitcoin.hdPrivatekey(self.master_seed, address_id)
@@ -43,17 +49,15 @@ class User:
 
 	# Get BIP32 address from site address
 	# Return: BIP32 auth address
-	def getAuthAddress(self, address):
-		return self.getSiteData(address)["auth_address"]
-
+	def getAuthAddress(self, address, create=True):
+		return self.getSiteData(address, create)["auth_address"]
 
-	def getAuthPrivatekey(self, address):
-		return self.getSiteData(address)["auth_privatekey"]
 
+	def getAuthPrivatekey(self, address, create=True):
+		return self.getSiteData(address, create)["auth_privatekey"]
 
 
 	# Set user attributes from dict
 	def setData(self, data):
 		for key, val in data.items():
 			setattr(self, key, val)
-

+ 72 - 59
src/User/UserManager.py

@@ -1,66 +1,79 @@
 import json, logging, os
 from User import User
+from Plugin import PluginManager
 
-users = None
-
-# Load all user from data/users.json
-def load():
-	global users
-	if not users: users = {}
-
-	user_found = []
-	added = 0
-	# Load new users
-	for master_address, data in json.load(open("data/users.json")).items():
-		if master_address not in users:
-			user = User(master_address)
-			user.setData(data)
-			users[master_address] = user
-			added += 1
-		user_found.append(master_address)
-
-	# Remove deleted adresses
-	for master_address in users.keys():
-		if master_address not in user_found: 
-			del(users[master_address])
-			logging.debug("Removed user: %s" % master_address)
-
-	if added: logging.debug("UserManager added %s users" % added)
-
-
-# Create new user
-# Return: User
-def create():
-	user = User()
-	logging.debug("Created user: %s" % user.master_address)
-	users[user.master_address] = user
-	user.save()
-	return user
-
-
-# List all users from data/users.json
-# Return: {"usermasteraddr": User}
-def list():
-	if users == None: # Not loaded yet
-		load()
-	return users
-
-
-# Get current authed user
-# Return: User
-def getCurrent():
-	users = list()
-	if users:
-		return users.values()[0]
-	else:
-		return create()
 
+@PluginManager.acceptPlugins
+class UserManager(object):
+	def __init__(self):
+		self.users = {}
+
+
+	# Load all user from data/users.json
+	def load(self):
+		if not self.users: self.users = {}
+
+		user_found = []
+		added = 0
+		# Load new users
+		for master_address, data in json.load(open("data/users.json")).items():
+			if master_address not in self.users:
+				user = User(master_address)
+				user.setData(data)
+				self.users[master_address] = user
+				added += 1
+			user_found.append(master_address)
+
+		# Remove deleted adresses
+		for master_address in self.users.keys():
+			if master_address not in user_found: 
+				del(self.users[master_address])
+				logging.debug("Removed user: %s" % master_address)
+
+		if added: logging.debug("UserManager added %s users" % added)
+
+
+	# Create new user
+	# Return: User
+	def create(self, master_address=None, master_seed=None):
+		user = User(master_address, master_seed)
+		logging.debug("Created user: %s" % user.master_address)
+		if user.master_address: # If successfully created
+			self.users[user.master_address] = user
+			user.save()
+		return user
+
+
+	# List all users from data/users.json
+	# Return: {"usermasteraddr": User}
+	def list(self):
+		if self.users == {}: # Not loaded yet
+			self.load()
+		return self.users
+
+
+	# Get user based on master_address
+	# Return: User or None
+	def get(self, master_address=None):
+		users = self.list()
+		if users:
+			return users.values()[0] # Single user mode, always return the first
+		else:
+			return None
+
+
+user_manager = UserManager() # Singletone
 
 # Debug: Reload User.py
-def reload():
-	return False # Disabled
-	"""import imp
-	global users, User
+def reloadModule():
+	return "Not used"
+	
+	import imp
+	global User, UserManager, user_manager
 	User = imp.load_source("User", "src/User/User.py").User # Reload source
-	users.clear() # Remove all items
-	load()"""
+	#module = imp.load_source("UserManager", "src/User/UserManager.py") # Reload module
+	#UserManager = module.UserManager
+	#user_manager = module.user_manager
+	# Reload users
+	user_manager = UserManager()
+	user_manager.load()

+ 20 - 9
src/main.py

@@ -1,6 +1,5 @@
 import os, sys
-update_after_shutdown = False
-sys.path.insert(0, os.path.dirname(__file__)) # Imports relative to main.py
+update_after_shutdown = False # If set True then update and restart zeronet after main loop ended
 
 # Create necessary files and dirs
 if not os.path.isdir("log"): os.mkdir("log")
@@ -11,7 +10,8 @@ if not os.path.isfile("data/users.json"): open("data/users.json", "w").write("{}
 # Load config
 from Config import config
 
-# Init logging
+
+# Setup logging
 import logging
 if config.action == "main":
 	if os.path.isfile("log/debug.log"):  # Simple logrotate
@@ -28,24 +28,35 @@ if config.action == "main": # Add time if main action
 else:
 	console_log.setFormatter(logging.Formatter('%(name)s %(message)s', "%H:%M:%S"))
 
-
 logging.getLogger('').addHandler(console_log) # Add console logger
 logging.getLogger('').name = "-" # Remove root prefix
 
+
 # Debug dependent configuration
 from Debug import DebugHook
 if config.debug:
-	console_log.setLevel(logging.DEBUG)
+	console_log.setLevel(logging.DEBUG) # Display everything to console
 	from gevent import monkey; monkey.patch_all(thread=False) # thread=False because of pyfilesystem
 else:
-	console_log.setLevel(logging.INFO)
-	from gevent import monkey; monkey.patch_all()
+	console_log.setLevel(logging.INFO) # Display only important info to console
+	from gevent import monkey; monkey.patch_all() # Make time, thread, socket gevent compatible
+
 
 import gevent
 import time
 
+
+# Log current config
 logging.debug("Config: %s" % config)
 
+
+# Load plugins
+from Plugin import PluginManager
+PluginManager.plugin_manager.loadPlugins()
+
+
+# -- Actions --
+
 # Starts here when running zeronet.py
 def start():
 	action_func = globals()[config.action] # Function reference
@@ -74,7 +85,7 @@ def main():
 
 def siteCreate():
 	logging.info("Generating new privatekey...")
-	from src.Crypt import CryptBitcoin
+	from Crypt import CryptBitcoin
 	privatekey = CryptBitcoin.newPrivatekey()
 	logging.info("----------------------------------------------------------------------")
 	logging.info("Site private key: %s" % privatekey)
@@ -196,7 +207,7 @@ def sitePublish(address, peer_ip=None, peer_port=15441, inner_path="content.json
 # Crypto commands
 
 def cryptoPrivatekeyToAddress(privatekey=None):
-	from src.Crypt import CryptBitcoin
+	from Crypt import CryptBitcoin
 	if not privatekey: # If no privatekey in args then ask it now
 		import getpass
 		privatekey = getpass.getpass("Private key (input hidden):")

+ 26 - 0
update.py

@@ -16,15 +16,41 @@ def update():
 		if not buff: break
 		data.write(buff)
 		print ".",
+	print "Downloaded."
+
+	# Checking plugins
+	plugins_enabled = []
+	plugins_disabled = []
+	if os.path.isdir("plugins"):
+		for dir in os.listdir("plugins"):
+			if dir.startswith("disabled-"):
+				plugins_disabled.append(dir.replace("disabled-", ""))
+			else:
+				plugins_enabled.append(dir)
+		print "Plugins:", plugins_enabled, plugins_disabled
+
 
 	print "Extracting...",
 	zip = zipfile.ZipFile(data)
 	for inner_path in zip.namelist():
+		inner_path = inner_path.replace("\\", "/") # Make sure we have unix path
 		print ".",
 		dest_path = inner_path.replace("ZeroNet-master/", "")
 		if not dest_path: continue
 
+
+		# Keep plugin disabled/enabled status
+		match = re.match("plugins/([^/]+)", dest_path)
+		if match:
+			plugin_name = match.group(1).replace("disabled-","")
+			if plugin_name in plugins_enabled: # Plugin was enabled
+				dest_path = dest_path.replace("plugins/disabled-"+plugin_name, "plugins/"+plugin_name)
+			else: # Plugin was disabled
+				dest_path = dest_path.replace("plugins/"+plugin_name, "plugins/disabled-"+plugin_name)
+			print "P",
+
 		dest_dir = os.path.dirname(dest_path)
+
 		if dest_dir and not os.path.isdir(dest_dir):
 			os.makedirs(dest_dir)
 

+ 3 - 1
zeronet.py

@@ -2,8 +2,10 @@
 
 def main():
 	print " - Starging ZeroNet..."
+	import sys, os
 	try:
-		from src import main
+		sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) # Imports relative to src
+		import main
 		main.start()
 		if main.update_after_shutdown: # Updater
 			import update, sys, os, gc