Browse Source

Version 0.3.2, rev351, Sidebar to display site infos an modify settings, Per-site upload/download bytes statistics, Deny different origin media requests, Allow 10sec to finish query modifications, Websocket display errors to client instead of disconnecting, Allow specify notification id to server-side messages, Track every command response time

HelloZeroNet 8 years ago
parent
commit
b83d6ba2ff
43 changed files with 7220 additions and 39 deletions
  1. 7 6
      README.md
  2. 407 0
      plugins/Sidebar/SidebarPlugin.py
  3. 1 0
      plugins/Sidebar/__init__.py
  4. 46 0
      plugins/Sidebar/maxminddb/__init__.py
  5. 28 0
      plugins/Sidebar/maxminddb/compat.py
  6. 7 0
      plugins/Sidebar/maxminddb/const.py
  7. 173 0
      plugins/Sidebar/maxminddb/decoder.py
  8. 11 0
      plugins/Sidebar/maxminddb/errors.py
  9. 570 0
      plugins/Sidebar/maxminddb/extension/maxminddb.c
  10. 65 0
      plugins/Sidebar/maxminddb/file.py
  11. 1897 0
      plugins/Sidebar/maxminddb/ipaddr.py
  12. 221 0
      plugins/Sidebar/maxminddb/reader.py
  13. 60 0
      plugins/Sidebar/media-globe/Detector.js
  14. 12 0
      plugins/Sidebar/media-globe/Tween.js
  15. 891 0
      plugins/Sidebar/media-globe/all.js
  16. 424 0
      plugins/Sidebar/media-globe/globe.js
  17. 372 0
      plugins/Sidebar/media-globe/three.min.js
  18. BIN
      plugins/Sidebar/media-globe/world.jpg
  19. 23 0
      plugins/Sidebar/media/Class.coffee
  20. 89 0
      plugins/Sidebar/media/Scrollable.js
  21. 44 0
      plugins/Sidebar/media/Scrollbable.css
  22. 318 0
      plugins/Sidebar/media/Sidebar.coffee
  23. 96 0
      plugins/Sidebar/media/Sidebar.css
  24. 150 0
      plugins/Sidebar/media/all.css
  25. 882 0
      plugins/Sidebar/media/all.js
  26. 340 0
      plugins/Sidebar/media/morphdom.js
  27. 3 1
      plugins/Stats/StatsPlugin.py
  28. 3 0
      plugins/Zeroname/UiRequestPlugin.py
  29. 2 2
      src/Config.py
  30. 3 0
      src/Connection/Connection.py
  31. 2 2
      src/Db/Db.py
  32. 14 7
      src/File/FileRequest.py
  33. 2 0
      src/Peer/Peer.py
  34. 8 5
      src/Site/Site.py
  35. 5 0
      src/Site/SiteStorage.py
  36. 15 9
      src/Ui/UiWebsocket.py
  37. 7 1
      src/Ui/media/Wrapper.coffee
  38. 5 1
      src/Ui/media/Wrapper.css
  39. 5 1
      src/Ui/media/all.css
  40. 10 2
      src/Ui/media/all.js
  41. BIN
      src/Ui/media/img/loading-circle.gif
  42. BIN
      src/Ui/media/img/loading.gif
  43. 2 2
      src/util/RateLimit.py

+ 7 - 6
README.md

@@ -18,7 +18,8 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - http://
  * Real-time updated sites
  * Namecoin .bit domains support
  * Easy to setup: unpack & run
- * Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 
+ * Clone websites in one click
+ * Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
    based authorization: Your account is protected by same cryptography as your Bitcoin wallet
  * Built-in SQL server with P2P data synchronization: Allows easier site development and faster page load times
  * Tor network support
@@ -26,12 +27,12 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - http://
  * Automatic, uPnP port opening
  * Plugin for multiuser (openproxy) support
  * Works with any browser/OS
- 
+
 
 ## How does it work?
 
 * After starting `zeronet.py` you will be able to visit zeronet sites using
-  `http://127.0.0.1:43110/{zeronet_address}` (eg. 
+  `http://127.0.0.1:43110/{zeronet_address}` (eg.
   `http://127.0.0.1:43110/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr`).
 * When you visit a new zeronet site, it tries to find peers using the BitTorrent
   network so it can download the site files (html, css, js...) from them.
@@ -82,7 +83,7 @@ It downloads the latest version of ZeroNet then starts it automatically.
 #### Debian
 
 * `sudo apt-get update`
-* `sudo apt-get install msgpack-python python-gevent` 
+* `sudo apt-get install msgpack-python python-gevent`
 * `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
 * `tar xvpfz master.tar.gz`
 * `cd ZeroNet-master`
@@ -91,7 +92,7 @@ It downloads the latest version of ZeroNet then starts it automatically.
 
 #### Other Linux or without root access
 * Check your python version using `python --version` if the returned version is not `Python 2.7.X` then try `python2` or `python2.7` command and use it from now
-* `wget https://bootstrap.pypa.io/get-pip.py` 
+* `wget https://bootstrap.pypa.io/get-pip.py`
 * `python get-pip.py --user gevent msgpack-python`
 * Start with `python zeronet.py`
 
@@ -101,7 +102,7 @@ It downloads the latest version of ZeroNet then starts it automatically.
  * `brew install python`
  * `pip install gevent msgpack-python`
  * [Download](https://github.com/HelloZeroNet/ZeroNet/archive/master.zip), Unpack, run `python zeronet.py`
- 
+
 ### Vagrant
 
 * `vagrant up`

+ 407 - 0
plugins/Sidebar/SidebarPlugin.py

@@ -0,0 +1,407 @@
+import re
+import os
+import cgi
+import sys
+import math
+import time
+try:
+    import cStringIO as StringIO
+except:
+    import StringIO
+
+
+from Config import config
+from Plugin import PluginManager
+from Debug import Debug
+
+plugin_dir = "plugins/Sidebar"
+media_dir = plugin_dir + "/media"
+sys.path.append(plugin_dir)  # To able to load geoip lib
+
+loc_cache = {}
+
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+    # Inject our resources to end of original file streams
+    def actionUiMedia(self, path):
+        if path == "/uimedia/all.js" or path == "/uimedia/all.css":
+            # First yield the original file and header
+            body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
+            for part in body_generator:
+                yield part
+
+            # Append our media file to the end
+            ext = re.match(".*(js|css)$", path).group(1)
+            plugin_media_file = "%s/all.%s" % (media_dir, ext)
+            if config.debug:
+                # If debugging merge *.css to all.css and *.js to all.js
+                from Debug import DebugMedia
+                DebugMedia.merge(plugin_media_file)
+            for part in self.actionFile(plugin_media_file, send_header=False):
+                yield part
+        elif path.startswith("/uimedia/globe/"):  # Serve WebGL globe files
+            file_name = re.match(".*/(.*)", path).group(1)
+            plugin_media_file = "%s-globe/%s" % (media_dir, file_name)
+            if config.debug and path.endswith("all.js"):
+                # If debugging merge *.css to all.css and *.js to all.js
+                from Debug import DebugMedia
+                DebugMedia.merge(plugin_media_file)
+            for part in self.actionFile(plugin_media_file):
+                yield part
+        else:
+            for part in super(UiRequestPlugin, self).actionUiMedia(path):
+                yield part
+
+
+@PluginManager.registerTo("UiWebsocket")
+class UiWebsocketPlugin(object):
+
+    def sidebarRenderPeerStats(self, body, site):
+        connected = len([peer for peer in site.peers.values() if peer.connection and peer.connection.connected])
+        connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")])
+        peers_total = len(site.peers)
+        if peers_total:
+            percent_connected = float(connected) / peers_total
+            percent_connectable = float(connectable) / peers_total
+        else:
+            percent_connectable = percent_connected = 0
+        body.append("""
+            <li>
+             <label>Peers</label>
+             <ul class='graph'>
+              <li style='width: 100%' class='total back-black' title="Total peers"></li>
+              <li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='Connectable peers'></li>
+              <li style='width: {percent_connected:.0%}' class='connected back-green' title='Connected peers'></li>
+             </ul>
+             <ul class='graph-legend'>
+              <li class='color-green'><span>connected:</span><b>{connected}</b></li>
+              <li class='color-blue'><span>Connectable:</span><b>{connectable}</b></li>
+              <li class='color-black'><span>Total:</span><b>{peers_total}</b></li>
+             </ul>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderTransferStats(self, body, site):
+        recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
+        sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
+        transfer_total = recv + sent
+        if transfer_total:
+            percent_recv = recv / transfer_total
+            percent_sent = sent / transfer_total
+        else:
+            percent_recv = 0.5
+            percent_sent = 0.5
+        body.append("""
+            <li>
+             <label>Data transfer</label>
+             <ul class='graph graph-stacked'>
+              <li style='width: {percent_recv:.0%}' class='received back-yellow' title="Received bytes"></li>
+              <li style='width: {percent_sent:.0%}' class='sent back-green' title="Sent bytes"></li>
+             </ul>
+             <ul class='graph-legend'>
+              <li class='color-yellow'><span>Received:</span><b>{recv:.2f}MB</b></li>
+              <li class='color-green'<span>Sent:</span><b>{sent:.2f}MB</b></li>
+             </ul>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderFileStats(self, body, site):
+        body.append("<li><label>Files</label><ul class='graph graph-stacked'>")
+
+        extensions = (
+            ("html", "yellow"),
+            ("css", "orange"),
+            ("js", "purple"),
+            ("image", "green"),
+            ("json", "blue"),
+            ("other", "white"),
+            ("total", "black")
+        )
+        # Collect stats
+        size_filetypes = {}
+        size_total = 0
+        for content in site.content_manager.contents.values():
+            if "files" not in content:
+                continue
+            for file_name, file_details in content["files"].items():
+                size_total += file_details["size"]
+                ext = file_name.split(".")[-1]
+                size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
+        size_other = size_total
+
+        # Bar
+        for extension, color in extensions:
+            if extension == "total":
+                continue
+            if extension == "other":
+                size = size_other
+            elif extension == "image":
+                size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
+                size_other -= size
+            else:
+                size = size_filetypes.get(extension, 0)
+                size_other -= size
+            percent = 100 * (float(size) / size_total)
+            body.append("<li style='width: %.0f%%' class='html back-%s' title='%s'></li>" % (percent, color, extension))
+
+        # Legend
+        body.append("</ul><ul class='graph-legend'>")
+        for extension, color in extensions:
+            if extension == "other":
+                size = size_other
+            elif extension == "image":
+                size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
+            elif extension == "total":
+                size = size_total
+            else:
+                size = size_filetypes.get(extension, 0)
+
+            if extension == "js":
+                title = "javascript"
+            else:
+                title = extension
+
+            if size > 1024 * 1024 * 10:  # Format as mB is more than 10mB
+                size_formatted = "%.0fMB" % (size / 1024 / 1024)
+            else:
+                size_formatted = "%.0fkB" % (size / 1024)
+
+            body.append("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, title, size_formatted))
+
+        body.append("</ul></li>")
+
+    def getFreeSpace(self):
+        free_space = 0
+        if "statvfs" in dir(os):  # Unix
+            statvfs = os.statvfs(config.data_dir)
+            free_space = statvfs.f_frsize * statvfs.f_bavail
+        else:  # Windows
+            try:
+                import ctypes
+                free_space_pointer = ctypes.c_ulonglong(0)
+                ctypes.windll.kernel32.GetDiskFreeSpaceExW(
+                    ctypes.c_wchar_p(config.data_dir), None, None, ctypes.pointer(free_space_pointer)
+                )
+                free_space = free_space_pointer.value
+            except Exception, err:
+                self.log.debug("GetFreeSpace error: %s" % err)
+        return free_space
+
+    def sidebarRenderSizeLimit(self, body, site):
+        free_space = self.getFreeSpace() / 1024 / 1024
+        size = float(site.settings["size"]) / 1024 / 1024
+        size_limit = site.getSizeLimit()
+        percent_used = size / size_limit
+        body.append("""
+            <li>
+             <label>Size limit <small>(limit used: {percent_used:.0%}, free space: {free_space:,d}MB)</small></label>
+             <input type='text' class='text text-num' value='{size_limit}' id='input-sitelimit'/><span class='text-post'>MB</span>
+             <a href='#Set' class='button' id='button-sitelimit'>Set</a>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderDbOptions(self, body, site):
+        if not site.storage.db:
+            return False
+
+        inner_path = site.storage.getInnerPath(site.storage.db.db_path)
+        size = float(site.storage.getSize(inner_path)) / 1024
+        body.append("""
+            <li>
+             <label>Database <small>({size:.2f}kB)</small></label>
+             <input type='text' class='text disabled' value='{inner_path}' disabled='disabled'/>
+             <a href='#Reindex' class='button' style='display: none'>Reindex</a>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderIdentity(self, body, site):
+        auth_address = self.user.getAuthAddress(self.site.address)
+        body.append("""
+            <li>
+             <label>Identity address</label>
+             <span class='input text disabled'>{auth_address}</span>
+             <a href='#Change' class='button' id='button-identity'>Change</a>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderOwnedCheckbox(self, body, site):
+        if self.site.settings["own"]:
+            checked = "checked='checked'"
+        else:
+            checked = ""
+
+        body.append("""
+            <h2 class='owned-title'>Owned site settings</h2>
+            <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
+        """.format(**locals()))
+
+    def sidebarRenderOwnSettings(self, body, site):
+        title = cgi.escape(site.content_manager.contents["content.json"]["title"], True)
+        description = cgi.escape(site.content_manager.contents["content.json"]["description"], True)
+        privatekey = cgi.escape(self.user.getSiteData(site.address, create=False).get("privatekey", ""))
+
+        body.append("""
+            <li>
+             <label for='settings-title'>Site title</label>
+             <input type='text' class='text' value="{title}" id='settings-title'/>
+            </li>
+
+            <li>
+             <label for='settings-description'>Site description</label>
+             <input type='text' class='text' value="{description}" id='settings-description'/>
+            </li>
+
+            <li style='display: none'>
+             <label>Private key</label>
+             <input type='text' class='text long' value="{privatekey}" placeholder='[Ask on signing]'/>
+            </li>
+
+            <li>
+             <a href='#Save' class='button' id='button-settings'>Save site settings</a>
+            </li>
+        """.format(**locals()))
+
+    def sidebarRenderContents(self, body, site):
+        body.append("""
+            <li>
+             <label>Content publishing</label>
+             <select id='select-contents'>
+        """)
+
+        for inner_path in sorted(site.content_manager.contents.keys()):
+            body.append("<option>%s</option>" % cgi.escape(inner_path, True))
+
+        body.append("""
+             </select>
+             <span class='select-down'>&rsaquo;</span>
+             <a href='#Sign' class='button' id='button-sign'>Sign</a>
+             <a href='#Publish' class='button' id='button-publish'>Publish</a>
+            </li>
+        """)
+
+    def actionSidebarGetHtmlTag(self, to):
+        site = self.site
+
+        body = []
+
+        body.append("<div>")
+        body.append("<h1>%s</h1>" % site.content_manager.contents["content.json"]["title"])
+
+        body.append("<div class='globe loading'></div>")
+
+        body.append("<ul class='fields'>")
+
+        self.sidebarRenderPeerStats(body, site)
+        self.sidebarRenderTransferStats(body, site)
+        self.sidebarRenderFileStats(body, site)
+        self.sidebarRenderSizeLimit(body, site)
+        self.sidebarRenderDbOptions(body, site)
+        self.sidebarRenderIdentity(body, site)
+
+        self.sidebarRenderOwnedCheckbox(body, site)
+        body.append("<div class='settings-owned'>")
+        self.sidebarRenderOwnSettings(body, site)
+        self.sidebarRenderContents(body, site)
+        body.append("</div>")
+        body.append("</ul>")
+        body.append("</div>")
+
+        self.response(to, "".join(body))
+
+    def downloadGeoLiteDb(self, db_path):
+        import urllib
+        import gzip
+        import shutil
+
+        self.log.info("Downloading GeoLite2 City database...")
+        self.cmd("notification", ["geolite-info", "Downloading GeoLite2 City database (one time only, ~15MB)...", 0])
+        try:
+            # Download
+            file = urllib.urlopen("http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz")
+            data = StringIO.StringIO()
+            while True:
+                buff = file.read(1024 * 16)
+                if not buff:
+                    break
+                data.write(buff)
+            self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
+            data.seek(0)
+
+            # Unpack
+            with gzip.GzipFile(fileobj=data) as gzip_file:
+                shutil.copyfileobj(gzip_file, open(db_path, "wb"))
+
+            self.cmd("notification", ["geolite-done", "GeoLite2 City database downloaded!", 5000])
+            time.sleep(2)  # Wait for notify animation
+        except Exception, err:
+            self.cmd("notification", ["geolite-error", "GeoLite2 City database download error: %s!" % err, 0])
+            raise err
+
+    def actionSidebarGetPeers(self, to):
+        permissions = self.getPermissions(to)
+        if "ADMIN" not in permissions:
+            return self.response(to, "You don't have permission to run this command")
+        try:
+            import maxminddb
+            db_path = config.data_dir + '/GeoLite2-City.mmdb'
+            if not os.path.isfile(db_path):
+                self.downloadGeoLiteDb(db_path)
+            geodb = maxminddb.open_database(db_path)
+
+            peers = self.site.peers.values()
+            # Find avg ping
+            ping_times = [
+                peer.connection.last_ping_delay
+                for peer in peers
+                if peer.connection and peer.connection.last_ping_delay and peer.connection.last_ping_delay
+            ]
+            if ping_times:
+                ping_avg = sum(ping_times) / float(len(ping_times))
+            else:
+                ping_avg = 0
+            # Place bars
+            globe_data = []
+            placed = {}  # Already placed bars here
+            for peer in peers:
+                # Height of bar
+                if peer.connection and peer.connection.last_ping_delay:
+                    ping = min(0.20, math.log(1 + peer.connection.last_ping_delay / ping_avg, 300))
+                else:
+                    ping = -0.03
+
+                # Query and cache location
+                if peer.ip in loc_cache:
+                    loc = loc_cache[peer.ip]
+                else:
+                    loc = geodb.get(peer.ip)
+                    loc_cache[peer.ip] = loc
+                if not loc:
+                    continue
+
+                # Create position array
+                lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
+                latlon = "%s,%s" % (lat, lon)
+                if latlon in placed:  # Dont place more than 1 bar to same place, fake repos using ip address last two part
+                    lat += float(128 - int(peer.ip.split(".")[-2])) / 50
+                    lon += float(128 - int(peer.ip.split(".")[-1])) / 50
+                    latlon = "%s,%s" % (lat, lon)
+                placed[latlon] = True
+
+                globe_data += (lat, lon, ping)
+            # Append myself
+            loc = geodb.get(config.ip_external)
+            if loc:
+                lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
+                globe_data += (lat, lon, -0.135)
+
+            self.response(to, globe_data)
+        except Exception, err:
+            self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
+            self.response(to, {"error": err})
+
+    def actionSiteSetOwned(self, to, owned):
+        permissions = self.getPermissions(to)
+        if "ADMIN" not in permissions:
+            return self.response(to, "You don't have permission to run this command")
+        self.site.settings["own"] = bool(owned)

+ 1 - 0
plugins/Sidebar/__init__.py

@@ -0,0 +1 @@
+import SidebarPlugin

+ 46 - 0
plugins/Sidebar/maxminddb/__init__.py

@@ -0,0 +1,46 @@
+# pylint:disable=C0111
+import os
+
+import maxminddb.reader
+
+try:
+    import maxminddb.extension
+except ImportError:
+    maxminddb.extension = None
+
+from maxminddb.const import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
+                             MODE_MEMORY)
+from maxminddb.decoder import InvalidDatabaseError
+
+
+def open_database(database, mode=MODE_AUTO):
+    """Open a Maxmind DB database
+
+    Arguments:
+        database -- A path to a valid MaxMind DB file such as a GeoIP2
+                    database file.
+        mode -- mode to open the database with. Valid mode are:
+            * MODE_MMAP_EXT - use the C extension with memory map.
+            * MODE_MMAP - read from memory map. Pure Python.
+            * MODE_FILE - read database as standard file. Pure Python.
+            * MODE_MEMORY - load database into memory. Pure Python.
+            * MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
+                          order. Default mode.
+    """
+    if (mode == MODE_AUTO and maxminddb.extension and
+            hasattr(maxminddb.extension, 'Reader')) or mode == MODE_MMAP_EXT:
+        return maxminddb.extension.Reader(database)
+    elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY):
+        return maxminddb.reader.Reader(database, mode)
+    raise ValueError('Unsupported open mode: {0}'.format(mode))
+
+
+def Reader(database):  # pylint: disable=invalid-name
+    """This exists for backwards compatibility. Use open_database instead"""
+    return open_database(database)
+
+__title__ = 'maxminddb'
+__version__ = '1.2.0'
+__author__ = 'Gregory Oschwald'
+__license__ = 'Apache License, Version 2.0'
+__copyright__ = 'Copyright 2014 Maxmind, Inc.'

+ 28 - 0
plugins/Sidebar/maxminddb/compat.py

@@ -0,0 +1,28 @@
+import sys
+
+# pylint: skip-file
+
+if sys.version_info[0] == 2:
+    import ipaddr as ipaddress  # pylint:disable=F0401
+    ipaddress.ip_address = ipaddress.IPAddress
+
+    int_from_byte = ord
+
+    FileNotFoundError = IOError
+
+    def int_from_bytes(b):
+        if b:
+            return int(b.encode("hex"), 16)
+        return 0
+
+    byte_from_int = chr
+else:
+    import ipaddress  # pylint:disable=F0401
+
+    int_from_byte = lambda x: x
+
+    FileNotFoundError = FileNotFoundError
+
+    int_from_bytes = lambda x: int.from_bytes(x, 'big')
+
+    byte_from_int = lambda x: bytes([x])

+ 7 - 0
plugins/Sidebar/maxminddb/const.py

@@ -0,0 +1,7 @@
+"""Constants used in the API"""
+
+MODE_AUTO = 0
+MODE_MMAP_EXT = 1
+MODE_MMAP = 2
+MODE_FILE = 4
+MODE_MEMORY = 8

+ 173 - 0
plugins/Sidebar/maxminddb/decoder.py

@@ -0,0 +1,173 @@
+"""
+maxminddb.decoder
+~~~~~~~~~~~~~~~~~
+
+This package contains code for decoding the MaxMind DB data section.
+
+"""
+from __future__ import unicode_literals
+
+import struct
+
+from maxminddb.compat import byte_from_int, int_from_bytes
+from maxminddb.errors import InvalidDatabaseError
+
+
+class Decoder(object):  # pylint: disable=too-few-public-methods
+
+    """Decoder for the data section of the MaxMind DB"""
+
+    def __init__(self, database_buffer, pointer_base=0, pointer_test=False):
+        """Created a Decoder for a MaxMind DB
+
+        Arguments:
+        database_buffer -- an mmap'd MaxMind DB file.
+        pointer_base -- the base number to use when decoding a pointer
+        pointer_test -- used for internal unit testing of pointer code
+        """
+        self._pointer_test = pointer_test
+        self._buffer = database_buffer
+        self._pointer_base = pointer_base
+
+    def _decode_array(self, size, offset):
+        array = []
+        for _ in range(size):
+            (value, offset) = self.decode(offset)
+            array.append(value)
+        return array, offset
+
+    def _decode_boolean(self, size, offset):
+        return size != 0, offset
+
+    def _decode_bytes(self, size, offset):
+        new_offset = offset + size
+        return self._buffer[offset:new_offset], new_offset
+
+    # pylint: disable=no-self-argument
+    # |-> I am open to better ways of doing this as long as it doesn't involve
+    #     lots of code duplication.
+    def _decode_packed_type(type_code, type_size, pad=False):
+        # pylint: disable=protected-access, missing-docstring
+        def unpack_type(self, size, offset):
+            if not pad:
+                self._verify_size(size, type_size)
+            new_offset = offset + type_size
+            packed_bytes = self._buffer[offset:new_offset]
+            if pad:
+                packed_bytes = packed_bytes.rjust(type_size, b'\x00')
+            (value,) = struct.unpack(type_code, packed_bytes)
+            return value, new_offset
+        return unpack_type
+
+    def _decode_map(self, size, offset):
+        container = {}
+        for _ in range(size):
+            (key, offset) = self.decode(offset)
+            (value, offset) = self.decode(offset)
+            container[key] = value
+        return container, offset
+
+    _pointer_value_offset = {
+        1: 0,
+        2: 2048,
+        3: 526336,
+        4: 0,
+    }
+
+    def _decode_pointer(self, size, offset):
+        pointer_size = ((size >> 3) & 0x3) + 1
+        new_offset = offset + pointer_size
+        pointer_bytes = self._buffer[offset:new_offset]
+        packed = pointer_bytes if pointer_size == 4 else struct.pack(
+            b'!c', byte_from_int(size & 0x7)) + pointer_bytes
+        unpacked = int_from_bytes(packed)
+        pointer = unpacked + self._pointer_base + \
+            self._pointer_value_offset[pointer_size]
+        if self._pointer_test:
+            return pointer, new_offset
+        (value, _) = self.decode(pointer)
+        return value, new_offset
+
+    def _decode_uint(self, size, offset):
+        new_offset = offset + size
+        uint_bytes = self._buffer[offset:new_offset]
+        return int_from_bytes(uint_bytes), new_offset
+
+    def _decode_utf8_string(self, size, offset):
+        new_offset = offset + size
+        return self._buffer[offset:new_offset].decode('utf-8'), new_offset
+
+    _type_decoder = {
+        1: _decode_pointer,
+        2: _decode_utf8_string,
+        3: _decode_packed_type(b'!d', 8),  # double,
+        4: _decode_bytes,
+        5: _decode_uint,  # uint16
+        6: _decode_uint,  # uint32
+        7: _decode_map,
+        8: _decode_packed_type(b'!i', 4, pad=True),  # int32
+        9: _decode_uint,  # uint64
+        10: _decode_uint,  # uint128
+        11: _decode_array,
+        14: _decode_boolean,
+        15: _decode_packed_type(b'!f', 4),  # float,
+    }
+
+    def decode(self, offset):
+        """Decode a section of the data section starting at offset
+
+        Arguments:
+        offset -- the location of the data structure to decode
+        """
+        new_offset = offset + 1
+        (ctrl_byte,) = struct.unpack(b'!B', self._buffer[offset:new_offset])
+        type_num = ctrl_byte >> 5
+        # Extended type
+        if not type_num:
+            (type_num, new_offset) = self._read_extended(new_offset)
+
+        if not type_num in self._type_decoder:
+            raise InvalidDatabaseError('Unexpected type number ({type}) '
+                                       'encountered'.format(type=type_num))
+
+        (size, new_offset) = self._size_from_ctrl_byte(
+            ctrl_byte, new_offset, type_num)
+        return self._type_decoder[type_num](self, size, new_offset)
+
+    def _read_extended(self, offset):
+        (next_byte,) = struct.unpack(b'!B', self._buffer[offset:offset + 1])
+        type_num = next_byte + 7
+        if type_num < 7:
+            raise InvalidDatabaseError(
+                'Something went horribly wrong in the decoder. An '
+                'extended type resolved to a type number < 8 '
+                '({type})'.format(type=type_num))
+        return type_num, offset + 1
+
+    def _verify_size(self, expected, actual):
+        if expected != actual:
+            raise InvalidDatabaseError(
+                'The MaxMind DB file\'s data section contains bad data '
+                '(unknown data type or corrupt data)'
+            )
+
+    def _size_from_ctrl_byte(self, ctrl_byte, offset, type_num):
+        size = ctrl_byte & 0x1f
+        if type_num == 1:
+            return size, offset
+        bytes_to_read = 0 if size < 29 else size - 28
+
+        new_offset = offset + bytes_to_read
+        size_bytes = self._buffer[offset:new_offset]
+
+        # Using unpack rather than int_from_bytes as it is about 200 lookups
+        # per second faster here.
+        if size == 29:
+            size = 29 + struct.unpack(b'!B', size_bytes)[0]
+        elif size == 30:
+            size = 285 + struct.unpack(b'!H', size_bytes)[0]
+        elif size > 30:
+            size = struct.unpack(
+                b'!I', size_bytes.rjust(4, b'\x00'))[0] + 65821
+
+        return size, new_offset

+ 11 - 0
plugins/Sidebar/maxminddb/errors.py

@@ -0,0 +1,11 @@
+"""
+maxminddb.errors
+~~~~~~~~~~~~~~~~
+
+This module contains custom errors for the MaxMind DB reader
+"""
+
+
+class InvalidDatabaseError(RuntimeError):
+
+    """This error is thrown when unexpected data is found in the database."""

+ 570 - 0
plugins/Sidebar/maxminddb/extension/maxminddb.c

@@ -0,0 +1,570 @@
+#include <Python.h>
+#include <maxminddb.h>
+#include "structmember.h"
+
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+
+static PyTypeObject Reader_Type;
+static PyTypeObject Metadata_Type;
+static PyObject *MaxMindDB_error;
+
+typedef struct {
+    PyObject_HEAD               /* no semicolon */
+    MMDB_s *mmdb;
+} Reader_obj;
+
+typedef struct {
+    PyObject_HEAD               /* no semicolon */
+    PyObject *binary_format_major_version;
+    PyObject *binary_format_minor_version;
+    PyObject *build_epoch;
+    PyObject *database_type;
+    PyObject *description;
+    PyObject *ip_version;
+    PyObject *languages;
+    PyObject *node_count;
+    PyObject *record_size;
+} Metadata_obj;
+
+static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list);
+static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list);
+static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list);
+static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list);
+
+#if PY_MAJOR_VERSION >= 3
+    #define MOD_INIT(name) PyMODINIT_FUNC PyInit_ ## name(void)
+    #define RETURN_MOD_INIT(m) return (m)
+    #define FILE_NOT_FOUND_ERROR PyExc_FileNotFoundError
+#else
+    #define MOD_INIT(name) PyMODINIT_FUNC init ## name(void)
+    #define RETURN_MOD_INIT(m) return
+    #define PyInt_FromLong PyLong_FromLong
+    #define FILE_NOT_FOUND_ERROR PyExc_IOError
+#endif
+
+#ifdef __GNUC__
+    #  define UNUSED(x) UNUSED_ ## x __attribute__((__unused__))
+#else
+    #  define UNUSED(x) UNUSED_ ## x
+#endif
+
+static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
+{
+    char *filename;
+    int mode = 0;
+
+    static char *kwlist[] = {"database", "mode", NULL};
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i", kwlist, &filename, &mode)) {
+        return -1;
+    }
+
+    if (mode != 0 && mode != 1) {
+        PyErr_Format(PyExc_ValueError, "Unsupported open mode (%i). Only "
+            "MODE_AUTO and MODE_MMAP_EXT are supported by this extension.",
+            mode);
+        return -1;
+    }
+
+    if (0 != access(filename, R_OK)) {
+        PyErr_Format(FILE_NOT_FOUND_ERROR,
+                     "No such file or directory: '%s'",
+                     filename);
+        return -1;
+    }
+
+    MMDB_s *mmdb = (MMDB_s *)malloc(sizeof(MMDB_s));
+    if (NULL == mmdb) {
+        PyErr_NoMemory();
+        return -1;
+    }
+
+    Reader_obj *mmdb_obj = (Reader_obj *)self;
+    if (!mmdb_obj) {
+        free(mmdb);
+        PyErr_NoMemory();
+        return -1;
+    }
+
+    uint16_t status = MMDB_open(filename, MMDB_MODE_MMAP, mmdb);
+
+    if (MMDB_SUCCESS != status) {
+        free(mmdb);
+        PyErr_Format(
+            MaxMindDB_error,
+            "Error opening database file (%s). Is this a valid MaxMind DB file?",
+            filename
+            );
+        return -1;
+    }
+
+    mmdb_obj->mmdb = mmdb;
+    return 0;
+}
+
+static PyObject *Reader_get(PyObject *self, PyObject *args)
+{
+    char *ip_address = NULL;
+
+    Reader_obj *mmdb_obj = (Reader_obj *)self;
+    if (!PyArg_ParseTuple(args, "s", &ip_address)) {
+        return NULL;
+    }
+
+    MMDB_s *mmdb = mmdb_obj->mmdb;
+
+    if (NULL == mmdb) {
+        PyErr_SetString(PyExc_ValueError,
+                        "Attempt to read from a closed MaxMind DB.");
+        return NULL;
+    }
+
+    int gai_error = 0;
+    int mmdb_error = MMDB_SUCCESS;
+    MMDB_lookup_result_s result =
+        MMDB_lookup_string(mmdb, ip_address, &gai_error,
+                           &mmdb_error);
+
+    if (0 != gai_error) {
+        PyErr_Format(PyExc_ValueError,
+                     "'%s' does not appear to be an IPv4 or IPv6 address.",
+                     ip_address);
+        return NULL;
+    }
+
+    if (MMDB_SUCCESS != mmdb_error) {
+        PyObject *exception;
+        if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
+            exception = PyExc_ValueError;
+        } else {
+            exception = MaxMindDB_error;
+        }
+        PyErr_Format(exception, "Error looking up %s. %s",
+                     ip_address, MMDB_strerror(mmdb_error));
+        return NULL;
+    }
+
+    if (!result.found_entry) {
+        Py_RETURN_NONE;
+    }
+
+    MMDB_entry_data_list_s *entry_data_list = NULL;
+    int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
+    if (MMDB_SUCCESS != status) {
+        PyErr_Format(MaxMindDB_error,
+                     "Error while looking up data for %s. %s",
+                     ip_address, MMDB_strerror(status));
+        MMDB_free_entry_data_list(entry_data_list);
+        return NULL;
+    }
+
+    MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
+    PyObject *py_obj = from_entry_data_list(&entry_data_list);
+    MMDB_free_entry_data_list(original_entry_data_list);
+    return py_obj;
+}
+
+static PyObject *Reader_metadata(PyObject *self, PyObject *UNUSED(args))
+{
+    Reader_obj *mmdb_obj = (Reader_obj *)self;
+
+    if (NULL == mmdb_obj->mmdb) {
+        PyErr_SetString(PyExc_IOError,
+                        "Attempt to read from a closed MaxMind DB.");
+        return NULL;
+    }
+
+    MMDB_entry_data_list_s *entry_data_list;
+    MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
+    MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
+
+    PyObject *metadata_dict = from_entry_data_list(&entry_data_list);
+    MMDB_free_entry_data_list(original_entry_data_list);
+    if (NULL == metadata_dict || !PyDict_Check(metadata_dict)) {
+        PyErr_SetString(MaxMindDB_error,
+                        "Error decoding metadata.");
+        return NULL;
+    }
+
+    PyObject *args = PyTuple_New(0);
+    if (NULL == args) {
+        Py_DECREF(metadata_dict);
+        return NULL;
+    }
+
+    PyObject *metadata = PyObject_Call((PyObject *)&Metadata_Type, args,
+                                       metadata_dict);
+
+    Py_DECREF(metadata_dict);
+    return metadata;
+}
+
+static PyObject *Reader_close(PyObject *self, PyObject *UNUSED(args))
+{
+    Reader_obj *mmdb_obj = (Reader_obj *)self;
+
+    if (NULL != mmdb_obj->mmdb) {
+        MMDB_close(mmdb_obj->mmdb);
+        free(mmdb_obj->mmdb);
+        mmdb_obj->mmdb = NULL;
+    }
+
+    Py_RETURN_NONE;
+}
+
+static void Reader_dealloc(PyObject *self)
+{
+    Reader_obj *obj = (Reader_obj *)self;
+    if (NULL != obj->mmdb) {
+        Reader_close(self, NULL);
+    }
+
+    PyObject_Del(self);
+}
+
+static int Metadata_init(PyObject *self, PyObject *args, PyObject *kwds)
+{
+
+    PyObject
+    *binary_format_major_version,
+    *binary_format_minor_version,
+    *build_epoch,
+    *database_type,
+    *description,
+    *ip_version,
+    *languages,
+    *node_count,
+    *record_size;
+
+    static char *kwlist[] = {
+        "binary_format_major_version",
+        "binary_format_minor_version",
+        "build_epoch",
+        "database_type",
+        "description",
+        "ip_version",
+        "languages",
+        "node_count",
+        "record_size",
+        NULL
+    };
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOOOO", kwlist,
+                                     &binary_format_major_version,
+                                     &binary_format_minor_version,
+                                     &build_epoch,
+                                     &database_type,
+                                     &description,
+                                     &ip_version,
+                                     &languages,
+                                     &node_count,
+                                     &record_size)) {
+        return -1;
+    }
+
+    Metadata_obj *obj = (Metadata_obj *)self;
+
+    obj->binary_format_major_version = binary_format_major_version;
+    obj->binary_format_minor_version = binary_format_minor_version;
+    obj->build_epoch = build_epoch;
+    obj->database_type = database_type;
+    obj->description = description;
+    obj->ip_version = ip_version;
+    obj->languages = languages;
+    obj->node_count = node_count;
+    obj->record_size = record_size;
+
+    Py_INCREF(obj->binary_format_major_version);
+    Py_INCREF(obj->binary_format_minor_version);
+    Py_INCREF(obj->build_epoch);
+    Py_INCREF(obj->database_type);
+    Py_INCREF(obj->description);
+    Py_INCREF(obj->ip_version);
+    Py_INCREF(obj->languages);
+    Py_INCREF(obj->node_count);
+    Py_INCREF(obj->record_size);
+
+    return 0;
+}
+
+static void Metadata_dealloc(PyObject *self)
+{
+    Metadata_obj *obj = (Metadata_obj *)self;
+    Py_DECREF(obj->binary_format_major_version);
+    Py_DECREF(obj->binary_format_minor_version);
+    Py_DECREF(obj->build_epoch);
+    Py_DECREF(obj->database_type);
+    Py_DECREF(obj->description);
+    Py_DECREF(obj->ip_version);
+    Py_DECREF(obj->languages);
+    Py_DECREF(obj->node_count);
+    Py_DECREF(obj->record_size);
+    PyObject_Del(self);
+}
+
+static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list)
+{
+    if (NULL == entry_data_list || NULL == *entry_data_list) {
+        PyErr_SetString(
+            MaxMindDB_error,
+            "Error while looking up data. Your database may be corrupt or you have found a bug in libmaxminddb."
+            );
+        return NULL;
+    }
+
+    switch ((*entry_data_list)->entry_data.type) {
+    case MMDB_DATA_TYPE_MAP:
+        return from_map(entry_data_list);
+    case MMDB_DATA_TYPE_ARRAY:
+        return from_array(entry_data_list);
+    case MMDB_DATA_TYPE_UTF8_STRING:
+        return PyUnicode_FromStringAndSize(
+                   (*entry_data_list)->entry_data.utf8_string,
+                   (*entry_data_list)->entry_data.data_size
+                   );
+    case MMDB_DATA_TYPE_BYTES:
+        return PyByteArray_FromStringAndSize(
+                   (const char *)(*entry_data_list)->entry_data.bytes,
+                   (Py_ssize_t)(*entry_data_list)->entry_data.data_size);
+    case MMDB_DATA_TYPE_DOUBLE:
+        return PyFloat_FromDouble((*entry_data_list)->entry_data.double_value);
+    case MMDB_DATA_TYPE_FLOAT:
+        return PyFloat_FromDouble((*entry_data_list)->entry_data.float_value);
+    case MMDB_DATA_TYPE_UINT16:
+        return PyLong_FromLong( (*entry_data_list)->entry_data.uint16);
+    case MMDB_DATA_TYPE_UINT32:
+        return PyLong_FromLong((*entry_data_list)->entry_data.uint32);
+    case MMDB_DATA_TYPE_BOOLEAN:
+        return PyBool_FromLong((*entry_data_list)->entry_data.boolean);
+    case MMDB_DATA_TYPE_UINT64:
+        return PyLong_FromUnsignedLongLong(
+                   (*entry_data_list)->entry_data.uint64);
+    case MMDB_DATA_TYPE_UINT128:
+        return from_uint128(*entry_data_list);
+    case MMDB_DATA_TYPE_INT32:
+        return PyLong_FromLong((*entry_data_list)->entry_data.int32);
+    default:
+        PyErr_Format(MaxMindDB_error,
+                     "Invalid data type arguments: %d",
+                     (*entry_data_list)->entry_data.type);
+        return NULL;
+    }
+    return NULL;
+}
+
+static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list)
+{
+    PyObject *py_obj = PyDict_New();
+    if (NULL == py_obj) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    const uint32_t map_size = (*entry_data_list)->entry_data.data_size;
+
+    uint i;
+    // entry_data_list cannot start out NULL (see from_entry_data_list). We
+    // check it in the loop because it may become NULL.
+    // coverity[check_after_deref]
+    for (i = 0; i < map_size && entry_data_list; i++) {
+        *entry_data_list = (*entry_data_list)->next;
+
+        PyObject *key = PyUnicode_FromStringAndSize(
+            (char *)(*entry_data_list)->entry_data.utf8_string,
+            (*entry_data_list)->entry_data.data_size
+            );
+
+        *entry_data_list = (*entry_data_list)->next;
+
+        PyObject *value = from_entry_data_list(entry_data_list);
+        if (NULL == value) {
+            Py_DECREF(key);
+            Py_DECREF(py_obj);
+            return NULL;
+        }
+        PyDict_SetItem(py_obj, key, value);
+        Py_DECREF(value);
+        Py_DECREF(key);
+    }
+
+    return py_obj;
+}
+
+static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list)
+{
+    const uint32_t size = (*entry_data_list)->entry_data.data_size;
+
+    PyObject *py_obj = PyList_New(size);
+    if (NULL == py_obj) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    uint i;
+    // entry_data_list cannot start out NULL (see from_entry_data_list). We
+    // check it in the loop because it may become NULL.
+    // coverity[check_after_deref]
+    for (i = 0; i < size && entry_data_list; i++) {
+        *entry_data_list = (*entry_data_list)->next;
+        PyObject *value = from_entry_data_list(entry_data_list);
+        if (NULL == value) {
+            Py_DECREF(py_obj);
+            return NULL;
+        }
+        // PyList_SetItem 'steals' the reference
+        PyList_SetItem(py_obj, i, value);
+    }
+    return py_obj;
+}
+
+static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list)
+{
+    uint64_t high = 0;
+    uint64_t low = 0;
+#if MMDB_UINT128_IS_BYTE_ARRAY
+    int i;
+    for (i = 0; i < 8; i++) {
+        high = (high << 8) | entry_data_list->entry_data.uint128[i];
+    }
+
+    for (i = 8; i < 16; i++) {
+        low = (low << 8) | entry_data_list->entry_data.uint128[i];
+    }
+#else
+    high = entry_data_list->entry_data.uint128 >> 64;
+    low = (uint64_t)entry_data_list->entry_data.uint128;
+#endif
+
+    char *num_str = malloc(33);
+    if (NULL == num_str) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    snprintf(num_str, 33, "%016" PRIX64 "%016" PRIX64, high, low);
+
+    PyObject *py_obj = PyLong_FromString(num_str, NULL, 16);
+
+    free(num_str);
+    return py_obj;
+}
+
+static PyMethodDef Reader_methods[] = {
+    { "get",      Reader_get,      METH_VARARGS,
+      "Get record for IP address" },
+    { "metadata", Reader_metadata, METH_NOARGS,
+      "Returns metadata object for database" },
+    { "close",    Reader_close,    METH_NOARGS, "Closes database"},
+    { NULL,       NULL,            0,           NULL        }
+};
+
+static PyTypeObject Reader_Type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_basicsize = sizeof(Reader_obj),
+    .tp_dealloc = Reader_dealloc,
+    .tp_doc = "Reader object",
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_methods = Reader_methods,
+    .tp_name = "Reader",
+    .tp_init = Reader_init,
+};
+
+static PyMethodDef Metadata_methods[] = {
+    { NULL, NULL, 0, NULL }
+};
+
+/* *INDENT-OFF* */
+static PyMemberDef Metadata_members[] = {
+    { "binary_format_major_version", T_OBJECT, offsetof(
+          Metadata_obj, binary_format_major_version), READONLY, NULL },
+    { "binary_format_minor_version", T_OBJECT, offsetof(
+          Metadata_obj, binary_format_minor_version), READONLY, NULL },
+    { "build_epoch", T_OBJECT, offsetof(Metadata_obj, build_epoch),
+          READONLY, NULL },
+    { "database_type", T_OBJECT, offsetof(Metadata_obj, database_type),
+          READONLY, NULL },
+    { "description", T_OBJECT, offsetof(Metadata_obj, description),
+          READONLY, NULL },
+    { "ip_version", T_OBJECT, offsetof(Metadata_obj, ip_version),
+          READONLY, NULL },
+    { "languages", T_OBJECT, offsetof(Metadata_obj, languages), READONLY,
+          NULL },
+    { "node_count", T_OBJECT, offsetof(Metadata_obj, node_count),
+          READONLY, NULL },
+    { "record_size", T_OBJECT, offsetof(Metadata_obj, record_size),
+          READONLY, NULL },
+    { NULL, 0, 0, 0, NULL }
+};
+/* *INDENT-ON* */
+
+static PyTypeObject Metadata_Type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_basicsize = sizeof(Metadata_obj),
+    .tp_dealloc = Metadata_dealloc,
+    .tp_doc = "Metadata object",
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_members = Metadata_members,
+    .tp_methods = Metadata_methods,
+    .tp_name = "Metadata",
+    .tp_init = Metadata_init
+};
+
+static PyMethodDef MaxMindDB_methods[] = {
+    { NULL, NULL, 0, NULL }
+};
+
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef MaxMindDB_module = {
+    PyModuleDef_HEAD_INIT,
+    .m_name = "extension",
+    .m_doc = "This is a C extension to read MaxMind DB file format",
+    .m_methods = MaxMindDB_methods,
+};
+#endif
+
+MOD_INIT(extension){
+    PyObject *m;
+
+#if PY_MAJOR_VERSION >= 3
+    m = PyModule_Create(&MaxMindDB_module);
+#else
+    m = Py_InitModule("extension", MaxMindDB_methods);
+#endif
+
+    if (!m) {
+        RETURN_MOD_INIT(NULL);
+    }
+
+    Reader_Type.tp_new = PyType_GenericNew;
+    if (PyType_Ready(&Reader_Type)) {
+        RETURN_MOD_INIT(NULL);
+    }
+    Py_INCREF(&Reader_Type);
+    PyModule_AddObject(m, "Reader", (PyObject *)&Reader_Type);
+
+    Metadata_Type.tp_new = PyType_GenericNew;
+    if (PyType_Ready(&Metadata_Type)) {
+        RETURN_MOD_INIT(NULL);
+    }
+    PyModule_AddObject(m, "extension", (PyObject *)&Metadata_Type);
+
+    PyObject* error_mod = PyImport_ImportModule("maxminddb.errors");
+    if (error_mod == NULL) {
+        RETURN_MOD_INIT(NULL);
+    }
+
+    MaxMindDB_error = PyObject_GetAttrString(error_mod, "InvalidDatabaseError");
+    Py_DECREF(error_mod);
+
+    if (MaxMindDB_error == NULL) {
+        RETURN_MOD_INIT(NULL);
+    }
+
+    Py_INCREF(MaxMindDB_error);
+
+    /* We primarily add it to the module for backwards compatibility */
+    PyModule_AddObject(m, "InvalidDatabaseError", MaxMindDB_error);
+
+    RETURN_MOD_INIT(m);
+}

+ 65 - 0
plugins/Sidebar/maxminddb/file.py

@@ -0,0 +1,65 @@
+"""For internal use only. It provides a slice-like file reader."""
+
+import os
+
+try:
+    from multiprocessing import Lock
+except ImportError:
+    from threading import Lock
+
+
+class FileBuffer(object):
+
+    """A slice-able file reader"""
+
+    def __init__(self, database):
+        self._handle = open(database, 'rb')
+        self._size = os.fstat(self._handle.fileno()).st_size
+        if not hasattr(os, 'pread'):
+            self._lock = Lock()
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            return self._read(key.stop - key.start, key.start)
+        elif isinstance(key, int):
+            return self._read(1, key)
+        else:
+            raise TypeError("Invalid argument type.")
+
+    def rfind(self, needle, start):
+        """Reverse find needle from start"""
+        pos = self._read(self._size - start - 1, start).rfind(needle)
+        if pos == -1:
+            return pos
+        return start + pos
+
+    def size(self):
+        """Size of file"""
+        return self._size
+
+    def close(self):
+        """Close file"""
+        self._handle.close()
+
+    if hasattr(os, 'pread'):
+
+        def _read(self, buffersize, offset):
+            """read that uses pread"""
+            # pylint: disable=no-member
+            return os.pread(self._handle.fileno(), buffersize, offset)
+
+    else:
+
+        def _read(self, buffersize, offset):
+            """read with a lock
+
+            This lock is necessary as after a fork, the different processes
+            will share the same file table entry, even if we dup the fd, and
+            as such the same offsets. There does not appear to be a way to
+            duplicate the file table entry and we cannot re-open based on the
+            original path as that file may have replaced with another or
+            unlinked.
+            """
+            with self._lock:
+                self._handle.seek(offset)
+                return self._handle.read(buffersize)

+ 1897 - 0
plugins/Sidebar/maxminddb/ipaddr.py

@@ -0,0 +1,1897 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc.
+#  Licensed to PSF under a Contributor Agreement.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+"""A fast, lightweight IPv4/IPv6 manipulation library in Python.
+
+This library is used to create/poke/manipulate IPv4 and IPv6 addresses
+and networks.
+
+"""
+
+__version__ = '2.1.10'
+
+import struct
+
+IPV4LENGTH = 32
+IPV6LENGTH = 128
+
+
+class AddressValueError(ValueError):
+    """A Value Error related to the address."""
+
+
+class NetmaskValueError(ValueError):
+    """A Value Error related to the netmask."""
+
+
+def IPAddress(address, version=None):
+    """Take an IP string/int and return an object of the correct type.
+
+    Args:
+        address: A string or integer, the IP address.  Either IPv4 or
+          IPv6 addresses may be supplied; integers less than 2**32 will
+          be considered to be IPv4 by default.
+        version: An Integer, 4 or 6. If set, don't try to automatically
+          determine what the IP address type is. important for things
+          like IPAddress(1), which could be IPv4, '0.0.0.1',  or IPv6,
+          '::1'.
+
+    Returns:
+        An IPv4Address or IPv6Address object.
+
+    Raises:
+        ValueError: if the string passed isn't either a v4 or a v6
+          address.
+
+    """
+    if version:
+        if version == 4:
+            return IPv4Address(address)
+        elif version == 6:
+            return IPv6Address(address)
+
+    try:
+        return IPv4Address(address)
+    except (AddressValueError, NetmaskValueError):
+        pass
+
+    try:
+        return IPv6Address(address)
+    except (AddressValueError, NetmaskValueError):
+        pass
+
+    raise ValueError('%r does not appear to be an IPv4 or IPv6 address' %
+                     address)
+
+
+def IPNetwork(address, version=None, strict=False):
+    """Take an IP string/int and return an object of the correct type.
+
+    Args:
+        address: A string or integer, the IP address.  Either IPv4 or
+          IPv6 addresses may be supplied; integers less than 2**32 will
+          be considered to be IPv4 by default.
+        version: An Integer, if set, don't try to automatically
+          determine what the IP address type is. important for things
+          like IPNetwork(1), which could be IPv4, '0.0.0.1/32', or IPv6,
+          '::1/128'.
+
+    Returns:
+        An IPv4Network or IPv6Network object.
+
+    Raises:
+        ValueError: if the string passed isn't either a v4 or a v6
+          address. Or if a strict network was requested and a strict
+          network wasn't given.
+
+    """
+    if version:
+        if version == 4:
+            return IPv4Network(address, strict)
+        elif version == 6:
+            return IPv6Network(address, strict)
+
+    try:
+        return IPv4Network(address, strict)
+    except (AddressValueError, NetmaskValueError):
+        pass
+
+    try:
+        return IPv6Network(address, strict)
+    except (AddressValueError, NetmaskValueError):
+        pass
+
+    raise ValueError('%r does not appear to be an IPv4 or IPv6 network' %
+                     address)
+
+
+def v4_int_to_packed(address):
+    """The binary representation of this address.
+
+    Args:
+        address: An integer representation of an IPv4 IP address.
+
+    Returns:
+        The binary representation of this address.
+
+    Raises:
+        ValueError: If the integer is too large to be an IPv4 IP
+          address.
+    """
+    if address > _BaseV4._ALL_ONES:
+        raise ValueError('Address too large for IPv4')
+    return Bytes(struct.pack('!I', address))
+
+
+def v6_int_to_packed(address):
+    """The binary representation of this address.
+
+    Args:
+        address: An integer representation of an IPv4 IP address.
+
+    Returns:
+        The binary representation of this address.
+    """
+    return Bytes(struct.pack('!QQ', address >> 64, address & (2**64 - 1)))
+
+
+def _find_address_range(addresses):
+    """Find a sequence of addresses.
+
+    Args:
+        addresses: a list of IPv4 or IPv6 addresses.
+
+    Returns:
+        A tuple containing the first and last IP addresses in the sequence.
+
+    """
+    first = last = addresses[0]
+    for ip in addresses[1:]:
+        if ip._ip == last._ip + 1:
+            last = ip
+        else:
+            break
+    return (first, last)
+
+def _get_prefix_length(number1, number2, bits):
+    """Get the number of leading bits that are same for two numbers.
+
+    Args:
+        number1: an integer.
+        number2: another integer.
+        bits: the maximum number of bits to compare.
+
+    Returns:
+        The number of leading bits that are the same for two numbers.
+
+    """
+    for i in range(bits):
+        if number1 >> i == number2 >> i:
+            return bits - i
+    return 0
+
+def _count_righthand_zero_bits(number, bits):
+    """Count the number of zero bits on the right hand side.
+
+    Args:
+        number: an integer.
+        bits: maximum number of bits to count.
+
+    Returns:
+        The number of zero bits on the right hand side of the number.
+
+    """
+    if number == 0:
+        return bits
+    for i in range(bits):
+        if (number >> i) % 2:
+            return i
+
+def summarize_address_range(first, last):
+    """Summarize a network range given the first and last IP addresses.
+
+    Example:
+        >>> summarize_address_range(IPv4Address('1.1.1.0'),
+            IPv4Address('1.1.1.130'))
+        [IPv4Network('1.1.1.0/25'), IPv4Network('1.1.1.128/31'),
+        IPv4Network('1.1.1.130/32')]
+
+    Args:
+        first: the first IPv4Address or IPv6Address in the range.
+        last: the last IPv4Address or IPv6Address in the range.
+
+    Returns:
+        The address range collapsed to a list of IPv4Network's or
+        IPv6Network's.
+
+    Raise:
+        TypeError:
+            If the first and last objects are not IP addresses.
+            If the first and last objects are not the same version.
+        ValueError:
+            If the last object is not greater than the first.
+            If the version is not 4 or 6.
+
+    """
+    if not (isinstance(first, _BaseIP) and isinstance(last, _BaseIP)):
+        raise TypeError('first and last must be IP addresses, not networks')
+    if first.version != last.version:
+        raise TypeError("%s and %s are not of the same version" % (
+                str(first), str(last)))
+    if first > last:
+        raise ValueError('last IP address must be greater than first')
+
+    networks = []
+
+    if first.version == 4:
+        ip = IPv4Network
+    elif first.version == 6:
+        ip = IPv6Network
+    else:
+        raise ValueError('unknown IP version')
+
+    ip_bits = first._max_prefixlen
+    first_int = first._ip
+    last_int = last._ip
+    while first_int <= last_int:
+        nbits = _count_righthand_zero_bits(first_int, ip_bits)
+        current = None
+        while nbits >= 0:
+            addend = 2**nbits - 1
+            current = first_int + addend
+            nbits -= 1
+            if current <= last_int:
+                break
+        prefix = _get_prefix_length(first_int, current, ip_bits)
+        net = ip('%s/%d' % (str(first), prefix))
+        networks.append(net)
+        if current == ip._ALL_ONES:
+            break
+        first_int = current + 1
+        first = IPAddress(first_int, version=first._version)
+    return networks
+
+def _collapse_address_list_recursive(addresses):
+    """Loops through the addresses, collapsing concurrent netblocks.
+
+    Example:
+
+        ip1 = IPv4Network('1.1.0.0/24')
+        ip2 = IPv4Network('1.1.1.0/24')
+        ip3 = IPv4Network('1.1.2.0/24')
+        ip4 = IPv4Network('1.1.3.0/24')
+        ip5 = IPv4Network('1.1.4.0/24')
+        ip6 = IPv4Network('1.1.0.1/22')
+
+        _collapse_address_list_recursive([ip1, ip2, ip3, ip4, ip5, ip6]) ->
+          [IPv4Network('1.1.0.0/22'), IPv4Network('1.1.4.0/24')]
+
+        This shouldn't be called directly; it is called via
+          collapse_address_list([]).
+
+    Args:
+        addresses: A list of IPv4Network's or IPv6Network's
+
+    Returns:
+        A list of IPv4Network's or IPv6Network's depending on what we were
+        passed.
+
+    """
+    ret_array = []
+    optimized = False
+
+    for cur_addr in addresses:
+        if not ret_array:
+            ret_array.append(cur_addr)
+            continue
+        if cur_addr in ret_array[-1]:
+            optimized = True
+        elif cur_addr == ret_array[-1].supernet().subnet()[1]:
+            ret_array.append(ret_array.pop().supernet())
+            optimized = True
+        else:
+            ret_array.append(cur_addr)
+
+    if optimized:
+        return _collapse_address_list_recursive(ret_array)
+
+    return ret_array
+
+
+def collapse_address_list(addresses):
+    """Collapse a list of IP objects.
+
+    Example:
+        collapse_address_list([IPv4('1.1.0.0/24'), IPv4('1.1.1.0/24')]) ->
+          [IPv4('1.1.0.0/23')]
+
+    Args:
+        addresses: A list of IPv4Network or IPv6Network objects.
+
+    Returns:
+        A list of IPv4Network or IPv6Network objects depending on what we
+        were passed.
+
+    Raises:
+        TypeError: If passed a list of mixed version objects.
+
+    """
+    i = 0
+    addrs = []
+    ips = []
+    nets = []
+
+    # split IP addresses and networks
+    for ip in addresses:
+        if isinstance(ip, _BaseIP):
+            if ips and ips[-1]._version != ip._version:
+                raise TypeError("%s and %s are not of the same version" % (
+                        str(ip), str(ips[-1])))
+            ips.append(ip)
+        elif ip._prefixlen == ip._max_prefixlen:
+            if ips and ips[-1]._version != ip._version:
+                raise TypeError("%s and %s are not of the same version" % (
+                        str(ip), str(ips[-1])))
+            ips.append(ip.ip)
+        else:
+            if nets and nets[-1]._version != ip._version:
+                raise TypeError("%s and %s are not of the same version" % (
+                        str(ip), str(ips[-1])))
+            nets.append(ip)
+
+    # sort and dedup
+    ips = sorted(set(ips))
+    nets = sorted(set(nets))
+
+    while i < len(ips):
+        (first, last) = _find_address_range(ips[i:])
+        i = ips.index(last) + 1
+        addrs.extend(summarize_address_range(first, last))
+
+    return _collapse_address_list_recursive(sorted(
+        addrs + nets, key=_BaseNet._get_networks_key))
+
+# backwards compatibility
+CollapseAddrList = collapse_address_list
+
+# We need to distinguish between the string and packed-bytes representations
+# of an IP address.  For example, b'0::1' is the IPv4 address 48.58.58.49,
+# while '0::1' is an IPv6 address.
+#
+# In Python 3, the native 'bytes' type already provides this functionality,
+# so we use it directly.  For earlier implementations where bytes is not a
+# distinct type, we create a subclass of str to serve as a tag.
+#
+# Usage example (Python 2):
+#   ip = ipaddr.IPAddress(ipaddr.Bytes('xxxx'))
+#
+# Usage example (Python 3):
+#   ip = ipaddr.IPAddress(b'xxxx')
+try:
+    if bytes is str:
+        raise TypeError("bytes is not a distinct type")
+    Bytes = bytes
+except (NameError, TypeError):
+    class Bytes(str):
+        def __repr__(self):
+            return 'Bytes(%s)' % str.__repr__(self)
+
+def get_mixed_type_key(obj):
+    """Return a key suitable for sorting between networks and addresses.
+
+    Address and Network objects are not sortable by default; they're
+    fundamentally different so the expression
+
+        IPv4Address('1.1.1.1') <= IPv4Network('1.1.1.1/24')
+
+    doesn't make any sense.  There are some times however, where you may wish
+    to have ipaddr sort these for you anyway. If you need to do this, you
+    can use this function as the key= argument to sorted().
+
+    Args:
+      obj: either a Network or Address object.
+    Returns:
+      appropriate key.
+
+    """
+    if isinstance(obj, _BaseNet):
+        return obj._get_networks_key()
+    elif isinstance(obj, _BaseIP):
+        return obj._get_address_key()
+    return NotImplemented
+
+class _IPAddrBase(object):
+
+    """The mother class."""
+
+    def __index__(self):
+        return self._ip
+
+    def __int__(self):
+        return self._ip
+
+    def __hex__(self):
+        return hex(self._ip)
+
+    @property
+    def exploded(self):
+        """Return the longhand version of the IP address as a string."""
+        return self._explode_shorthand_ip_string()
+
+    @property
+    def compressed(self):
+        """Return the shorthand version of the IP address as a string."""
+        return str(self)
+
+
+class _BaseIP(_IPAddrBase):
+
+    """A generic IP object.
+
+    This IP class contains the version independent methods which are
+    used by single IP addresses.
+
+    """
+
+    def __eq__(self, other):
+        try:
+            return (self._ip == other._ip
+                    and self._version == other._version)
+        except AttributeError:
+            return NotImplemented
+
+    def __ne__(self, other):
+        eq = self.__eq__(other)
+        if eq is NotImplemented:
+            return NotImplemented
+        return not eq
+
+    def __le__(self, other):
+        gt = self.__gt__(other)
+        if gt is NotImplemented:
+            return NotImplemented
+        return not gt
+
+    def __ge__(self, other):
+        lt = self.__lt__(other)
+        if lt is NotImplemented:
+            return NotImplemented
+        return not lt
+
+    def __lt__(self, other):
+        if self._version != other._version:
+            raise TypeError('%s and %s are not of the same version' % (
+                    str(self), str(other)))
+        if not isinstance(other, _BaseIP):
+            raise TypeError('%s and %s are not of the same type' % (
+                    str(self), str(other)))
+        if self._ip != other._ip:
+            return self._ip < other._ip
+        return False
+
+    def __gt__(self, other):
+        if self._version != other._version:
+            raise TypeError('%s and %s are not of the same version' % (
+                    str(self), str(other)))
+        if not isinstance(other, _BaseIP):
+            raise TypeError('%s and %s are not of the same type' % (
+                    str(self), str(other)))
+        if self._ip != other._ip:
+            return self._ip > other._ip
+        return False
+
+    # Shorthand for Integer addition and subtraction. This is not
+    # meant to ever support addition/subtraction of addresses.
+    def __add__(self, other):
+        if not isinstance(other, int):
+            return NotImplemented
+        return IPAddress(int(self) + other, version=self._version)
+
+    def __sub__(self, other):
+        if not isinstance(other, int):
+            return NotImplemented
+        return IPAddress(int(self) - other, version=self._version)
+
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, str(self))
+
+    def __str__(self):
+        return  '%s' % self._string_from_ip_int(self._ip)
+
+    def __hash__(self):
+        return hash(hex(long(self._ip)))
+
+    def _get_address_key(self):
+        return (self._version, self)
+
+    @property
+    def version(self):
+        raise NotImplementedError('BaseIP has no version')
+
+
+class _BaseNet(_IPAddrBase):
+
+    """A generic IP object.
+
+    This IP class contains the version independent methods which are
+    used by networks.
+
+    """
+
+    def __init__(self, address):
+        self._cache = {}
+
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, str(self))
+
+    def iterhosts(self):
+        """Generate Iterator over usable hosts in a network.
+
+           This is like __iter__ except it doesn't return the network
+           or broadcast addresses.
+
+        """
+        cur = int(self.network) + 1
+        bcast = int(self.broadcast) - 1
+        while cur <= bcast:
+            cur += 1
+            yield IPAddress(cur - 1, version=self._version)
+
+    def __iter__(self):
+        cur = int(self.network)
+        bcast = int(self.broadcast)
+        while cur <= bcast:
+            cur += 1
+            yield IPAddress(cur - 1, version=self._version)
+
+    def __getitem__(self, n):
+        network = int(self.network)
+        broadcast = int(self.broadcast)
+        if n >= 0:
+            if network + n > broadcast:
+                raise IndexError
+            return IPAddress(network + n, version=self._version)
+        else:
+            n += 1
+            if broadcast + n < network:
+                raise IndexError
+            return IPAddress(broadcast + n, version=self._version)
+
+    def __lt__(self, other):
+        if self._version != other._version:
+            raise TypeError('%s and %s are not of the same version' % (
+                    str(self), str(other)))
+        if not isinstance(other, _BaseNet):
+            raise TypeError('%s and %s are not of the same type' % (
+                    str(self), str(other)))
+        if self.network != other.network:
+            return self.network < other.network
+        if self.netmask != other.netmask:
+            return self.netmask < other.netmask
+        return False
+
+    def __gt__(self, other):
+        if self._version != other._version:
+            raise TypeError('%s and %s are not of the same version' % (
+                    str(self), str(other)))
+        if not isinstance(other, _BaseNet):
+            raise TypeError('%s and %s are not of the same type' % (
+                    str(self), str(other)))
+        if self.network != other.network:
+            return self.network > other.network
+        if self.netmask != other.netmask:
+            return self.netmask > other.netmask
+        return False
+
+    def __le__(self, other):
+        gt = self.__gt__(other)
+        if gt is NotImplemented:
+            return NotImplemented
+        return not gt
+
+    def __ge__(self, other):
+        lt = self.__lt__(other)
+        if lt is NotImplemented:
+            return NotImplemented
+        return not lt
+
+    def __eq__(self, other):
+        try:
+            return (self._version == other._version
+                    and self.network == other.network
+                    and int(self.netmask) == int(other.netmask))
+        except AttributeError:
+            if isinstance(other, _BaseIP):
+                return (self._version == other._version
+                        and self._ip == other._ip)
+
+    def __ne__(self, other):
+        eq = self.__eq__(other)
+        if eq is NotImplemented:
+            return NotImplemented
+        return not eq
+
+    def __str__(self):
+        return  '%s/%s' % (str(self.ip),
+                           str(self._prefixlen))
+
+    def __hash__(self):
+        return hash(int(self.network) ^ int(self.netmask))
+
+    def __contains__(self, other):
+        # always false if one is v4 and the other is v6.
+        if self._version != other._version:
+          return False
+        # dealing with another network.
+        if isinstance(other, _BaseNet):
+            return (self.network <= other.network and
+                    self.broadcast >= other.broadcast)
+        # dealing with another address
+        else:
+            return (int(self.network) <= int(other._ip) <=
+                    int(self.broadcast))
+
+    def overlaps(self, other):
+        """Tell if self is partly contained in other."""
+        return self.network in other or self.broadcast in other or (
+            other.network in self or other.broadcast in self)
+
+    @property
+    def network(self):
+        x = self._cache.get('network')
+        if x is None:
+            x = IPAddress(self._ip & int(self.netmask), version=self._version)
+            self._cache['network'] = x
+        return x
+
+    @property
+    def broadcast(self):
+        x = self._cache.get('broadcast')
+        if x is None:
+            x = IPAddress(self._ip | int(self.hostmask), version=self._version)
+            self._cache['broadcast'] = x
+        return x
+
+    @property
+    def hostmask(self):
+        x = self._cache.get('hostmask')
+        if x is None:
+            x = IPAddress(int(self.netmask) ^ self._ALL_ONES,
+                          version=self._version)
+            self._cache['hostmask'] = x
+        return x
+
+    @property
+    def with_prefixlen(self):
+        return '%s/%d' % (str(self.ip), self._prefixlen)
+
+    @property
+    def with_netmask(self):
+        return '%s/%s' % (str(self.ip), str(self.netmask))
+
+    @property
+    def with_hostmask(self):
+        return '%s/%s' % (str(self.ip), str(self.hostmask))
+
+    @property
+    def numhosts(self):
+        """Number of hosts in the current subnet."""
+        return int(self.broadcast) - int(self.network) + 1
+
+    @property
+    def version(self):
+        raise NotImplementedError('BaseNet has no version')
+
+    @property
+    def prefixlen(self):
+        return self._prefixlen
+
+    def address_exclude(self, other):
+        """Remove an address from a larger block.
+
+        For example:
+
+            addr1 = IPNetwork('10.1.1.0/24')
+            addr2 = IPNetwork('10.1.1.0/26')
+            addr1.address_exclude(addr2) =
+                [IPNetwork('10.1.1.64/26'), IPNetwork('10.1.1.128/25')]
+
+        or IPv6:
+
+            addr1 = IPNetwork('::1/32')
+            addr2 = IPNetwork('::1/128')
+            addr1.address_exclude(addr2) = [IPNetwork('::0/128'),
+                IPNetwork('::2/127'),
+                IPNetwork('::4/126'),
+                IPNetwork('::8/125'),
+                ...
+                IPNetwork('0:0:8000::/33')]
+
+        Args:
+            other: An IPvXNetwork object of the same type.
+
+        Returns:
+            A sorted list of IPvXNetwork objects addresses which is self
+            minus other.
+
+        Raises:
+            TypeError: If self and other are of difffering address
+              versions, or if other is not a network object.
+            ValueError: If other is not completely contained by self.
+
+        """
+        if not self._version == other._version:
+            raise TypeError("%s and %s are not of the same version" % (
+                str(self), str(other)))
+
+        if not isinstance(other, _BaseNet):
+            raise TypeError("%s is not a network object" % str(other))
+
+        if other not in self:
+            raise ValueError('%s not contained in %s' % (str(other),
+                                                         str(self)))
+        if other == self:
+            return []
+
+        ret_addrs = []
+
+        # Make sure we're comparing the network of other.
+        other = IPNetwork('%s/%s' % (str(other.network), str(other.prefixlen)),
+                   version=other._version)
+
+        s1, s2 = self.subnet()
+        while s1 != other and s2 != other:
+            if other in s1:
+                ret_addrs.append(s2)
+                s1, s2 = s1.subnet()
+            elif other in s2:
+                ret_addrs.append(s1)
+                s1, s2 = s2.subnet()
+            else:
+                # If we got here, there's a bug somewhere.
+                assert True == False, ('Error performing exclusion: '
+                                       's1: %s s2: %s other: %s' %
+                                       (str(s1), str(s2), str(other)))
+        if s1 == other:
+            ret_addrs.append(s2)
+        elif s2 == other:
+            ret_addrs.append(s1)
+        else:
+            # If we got here, there's a bug somewhere.
+            assert True == False, ('Error performing exclusion: '
+                                   's1: %s s2: %s other: %s' %
+                                   (str(s1), str(s2), str(other)))
+
+        return sorted(ret_addrs, key=_BaseNet._get_networks_key)
+
+    def compare_networks(self, other):
+        """Compare two IP objects.
+
+        This is only concerned about the comparison of the integer
+        representation of the network addresses.  This means that the
+        host bits aren't considered at all in this method.  If you want
+        to compare host bits, you can easily enough do a
+        'HostA._ip < HostB._ip'
+
+        Args:
+            other: An IP object.
+
+        Returns:
+            If the IP versions of self and other are the same, returns:
+
+            -1 if self < other:
+              eg: IPv4('1.1.1.0/24') < IPv4('1.1.2.0/24')
+              IPv6('1080::200C:417A') < IPv6('1080::200B:417B')
+            0 if self == other
+              eg: IPv4('1.1.1.1/24') == IPv4('1.1.1.2/24')
+              IPv6('1080::200C:417A/96') == IPv6('1080::200C:417B/96')
+            1 if self > other
+              eg: IPv4('1.1.1.0/24') > IPv4('1.1.0.0/24')
+              IPv6('1080::1:200C:417A/112') >
+              IPv6('1080::0:200C:417A/112')
+
+            If the IP versions of self and other are different, returns:
+
+            -1 if self._version < other._version
+              eg: IPv4('10.0.0.1/24') < IPv6('::1/128')
+            1 if self._version > other._version
+              eg: IPv6('::1/128') > IPv4('255.255.255.0/24')
+
+        """
+        if self._version < other._version:
+            return -1
+        if self._version > other._version:
+            return 1
+        # self._version == other._version below here:
+        if self.network < other.network:
+            return -1
+        if self.network > other.network:
+            return 1
+        # self.network == other.network below here:
+        if self.netmask < other.netmask:
+            return -1
+        if self.netmask > other.netmask:
+            return 1
+        # self.network == other.network and self.netmask == other.netmask
+        return 0
+
+    def _get_networks_key(self):
+        """Network-only key function.
+
+        Returns an object that identifies this address' network and
+        netmask. This function is a suitable "key" argument for sorted()
+        and list.sort().
+
+        """
+        return (self._version, self.network, self.netmask)
+
+    def _ip_int_from_prefix(self, prefixlen=None):
+        """Turn the prefix length netmask into a int for comparison.
+
+        Args:
+            prefixlen: An integer, the prefix length.
+
+        Returns:
+            An integer.
+
+        """
+        if not prefixlen and prefixlen != 0:
+            prefixlen = self._prefixlen
+        return self._ALL_ONES ^ (self._ALL_ONES >> prefixlen)
+
+    def _prefix_from_ip_int(self, ip_int, mask=32):
+        """Return prefix length from the decimal netmask.
+
+        Args:
+            ip_int: An integer, the IP address.
+            mask: The netmask.  Defaults to 32.
+
+        Returns:
+            An integer, the prefix length.
+
+        """
+        while mask:
+            if ip_int & 1 == 1:
+                break
+            ip_int >>= 1
+            mask -= 1
+
+        return mask
+
+    def _ip_string_from_prefix(self, prefixlen=None):
+        """Turn a prefix length into a dotted decimal string.
+
+        Args:
+            prefixlen: An integer, the netmask prefix length.
+
+        Returns:
+            A string, the dotted decimal netmask string.
+
+        """
+        if not prefixlen:
+            prefixlen = self._prefixlen
+        return self._string_from_ip_int(self._ip_int_from_prefix(prefixlen))
+
+    def iter_subnets(self, prefixlen_diff=1, new_prefix=None):
+        """The subnets which join to make the current subnet.
+
+        In the case that self contains only one IP
+        (self._prefixlen == 32 for IPv4 or self._prefixlen == 128
+        for IPv6), return a list with just ourself.
+
+        Args:
+            prefixlen_diff: An integer, the amount the prefix length
+              should be increased by. This should not be set if
+              new_prefix is also set.
+            new_prefix: The desired new prefix length. This must be a
+              larger number (smaller prefix) than the existing prefix.
+              This should not be set if prefixlen_diff is also set.
+
+        Returns:
+            An iterator of IPv(4|6) objects.
+
+        Raises:
+            ValueError: The prefixlen_diff is too small or too large.
+                OR
+            prefixlen_diff and new_prefix are both set or new_prefix
+              is a smaller number than the current prefix (smaller
+              number means a larger network)
+
+        """
+        if self._prefixlen == self._max_prefixlen:
+            yield self
+            return
+
+        if new_prefix is not None:
+            if new_prefix < self._prefixlen:
+                raise ValueError('new prefix must be longer')
+            if prefixlen_diff != 1:
+                raise ValueError('cannot set prefixlen_diff and new_prefix')
+            prefixlen_diff = new_prefix - self._prefixlen
+
+        if prefixlen_diff < 0:
+            raise ValueError('prefix length diff must be > 0')
+        new_prefixlen = self._prefixlen + prefixlen_diff
+
+        if not self._is_valid_netmask(str(new_prefixlen)):
+            raise ValueError(
+                'prefix length diff %d is invalid for netblock %s' % (
+                    new_prefixlen, str(self)))
+
+        first = IPNetwork('%s/%s' % (str(self.network),
+                                     str(self._prefixlen + prefixlen_diff)),
+                         version=self._version)
+
+        yield first
+        current = first
+        while True:
+            broadcast = current.broadcast
+            if broadcast == self.broadcast:
+                return
+            new_addr = IPAddress(int(broadcast) + 1, version=self._version)
+            current = IPNetwork('%s/%s' % (str(new_addr), str(new_prefixlen)),
+                                version=self._version)
+
+            yield current
+
+    def masked(self):
+        """Return the network object with the host bits masked out."""
+        return IPNetwork('%s/%d' % (self.network, self._prefixlen),
+                         version=self._version)
+
+    def subnet(self, prefixlen_diff=1, new_prefix=None):
+        """Return a list of subnets, rather than an iterator."""
+        return list(self.iter_subnets(prefixlen_diff, new_prefix))
+
+    def supernet(self, prefixlen_diff=1, new_prefix=None):
+        """The supernet containing the current network.
+
+        Args:
+            prefixlen_diff: An integer, the amount the prefix length of
+              the network should be decreased by.  For example, given a
+              /24 network and a prefixlen_diff of 3, a supernet with a
+              /21 netmask is returned.
+
+        Returns:
+            An IPv4 network object.
+
+        Raises:
+            ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have a
+              negative prefix length.
+                OR
+            If prefixlen_diff and new_prefix are both set or new_prefix is a
+              larger number than the current prefix (larger number means a
+              smaller network)
+
+        """
+        if self._prefixlen == 0:
+            return self
+
+        if new_prefix is not None:
+            if new_prefix > self._prefixlen:
+                raise ValueError('new prefix must be shorter')
+            if prefixlen_diff != 1:
+                raise ValueError('cannot set prefixlen_diff and new_prefix')
+            prefixlen_diff = self._prefixlen - new_prefix
+
+
+        if self.prefixlen - prefixlen_diff < 0:
+            raise ValueError(
+                'current prefixlen is %d, cannot have a prefixlen_diff of %d' %
+                (self.prefixlen, prefixlen_diff))
+        return IPNetwork('%s/%s' % (str(self.network),
+                                    str(self.prefixlen - prefixlen_diff)),
+                         version=self._version)
+
+    # backwards compatibility
+    Subnet = subnet
+    Supernet = supernet
+    AddressExclude = address_exclude
+    CompareNetworks = compare_networks
+    Contains = __contains__
+
+
+class _BaseV4(object):
+
+    """Base IPv4 object.
+
+    The following methods are used by IPv4 objects in both single IP
+    addresses and networks.
+
+    """
+
+    # Equivalent to 255.255.255.255 or 32 bits of 1's.
+    _ALL_ONES = (2**IPV4LENGTH) - 1
+    _DECIMAL_DIGITS = frozenset('0123456789')
+
+    def __init__(self, address):
+        self._version = 4
+        self._max_prefixlen = IPV4LENGTH
+
+    def _explode_shorthand_ip_string(self):
+        return str(self)
+
+    def _ip_int_from_string(self, ip_str):
+        """Turn the given IP string into an integer for comparison.
+
+        Args:
+            ip_str: A string, the IP ip_str.
+
+        Returns:
+            The IP ip_str as an integer.
+
+        Raises:
+            AddressValueError: if ip_str isn't a valid IPv4 Address.
+
+        """
+        octets = ip_str.split('.')
+        if len(octets) != 4:
+            raise AddressValueError(ip_str)
+
+        packed_ip = 0
+        for oc in octets:
+            try:
+                packed_ip = (packed_ip << 8) | self._parse_octet(oc)
+            except ValueError:
+                raise AddressValueError(ip_str)
+        return packed_ip
+
+    def _parse_octet(self, octet_str):
+        """Convert a decimal octet into an integer.
+
+        Args:
+            octet_str: A string, the number to parse.
+
+        Returns:
+            The octet as an integer.
+
+        Raises:
+            ValueError: if the octet isn't strictly a decimal from [0..255].
+
+        """
+        # Whitelist the characters, since int() allows a lot of bizarre stuff.
+        if not self._DECIMAL_DIGITS.issuperset(octet_str):
+            raise ValueError
+        octet_int = int(octet_str, 10)
+        # Disallow leading zeroes, because no clear standard exists on
+        # whether these should be interpreted as decimal or octal.
+        if octet_int > 255 or (octet_str[0] == '0' and len(octet_str) > 1):
+            raise ValueError
+        return octet_int
+
+    def _string_from_ip_int(self, ip_int):
+        """Turns a 32-bit integer into dotted decimal notation.
+
+        Args:
+            ip_int: An integer, the IP address.
+
+        Returns:
+            The IP address as a string in dotted decimal notation.
+
+        """
+        octets = []
+        for _ in xrange(4):
+            octets.insert(0, str(ip_int & 0xFF))
+            ip_int >>= 8
+        return '.'.join(octets)
+
+    @property
+    def max_prefixlen(self):
+        return self._max_prefixlen
+
+    @property
+    def packed(self):
+        """The binary representation of this address."""
+        return v4_int_to_packed(self._ip)
+
+    @property
+    def version(self):
+        return self._version
+
+    @property
+    def is_reserved(self):
+       """Test if the address is otherwise IETF reserved.
+
+        Returns:
+            A boolean, True if the address is within the
+            reserved IPv4 Network range.
+
+       """
+       return self in IPv4Network('240.0.0.0/4')
+
+    @property
+    def is_private(self):
+        """Test if this address is allocated for private networks.
+
+        Returns:
+            A boolean, True if the address is reserved per RFC 1918.
+
+        """
+        return (self in IPv4Network('10.0.0.0/8') or
+                self in IPv4Network('172.16.0.0/12') or
+                self in IPv4Network('192.168.0.0/16'))
+
+    @property
+    def is_multicast(self):
+        """Test if the address is reserved for multicast use.
+
+        Returns:
+            A boolean, True if the address is multicast.
+            See RFC 3171 for details.
+
+        """
+        return self in IPv4Network('224.0.0.0/4')
+
+    @property
+    def is_unspecified(self):
+        """Test if the address is unspecified.
+
+        Returns:
+            A boolean, True if this is the unspecified address as defined in
+            RFC 5735 3.
+
+        """
+        return self in IPv4Network('0.0.0.0')
+
+    @property
+    def is_loopback(self):
+        """Test if the address is a loopback address.
+
+        Returns:
+            A boolean, True if the address is a loopback per RFC 3330.
+
+        """
+        return self in IPv4Network('127.0.0.0/8')
+
+    @property
+    def is_link_local(self):
+        """Test if the address is reserved for link-local.
+
+        Returns:
+            A boolean, True if the address is link-local per RFC 3927.
+
+        """
+        return self in IPv4Network('169.254.0.0/16')
+
+
+class IPv4Address(_BaseV4, _BaseIP):
+
+    """Represent and manipulate single IPv4 Addresses."""
+
+    def __init__(self, address):
+
+        """
+        Args:
+            address: A string or integer representing the IP
+              '192.168.1.1'
+
+              Additionally, an integer can be passed, so
+              IPv4Address('192.168.1.1') == IPv4Address(3232235777).
+              or, more generally
+              IPv4Address(int(IPv4Address('192.168.1.1'))) ==
+                IPv4Address('192.168.1.1')
+
+        Raises:
+            AddressValueError: If ipaddr isn't a valid IPv4 address.
+
+        """
+        _BaseV4.__init__(self, address)
+
+        # Efficient constructor from integer.
+        if isinstance(address, (int, long)):
+            self._ip = address
+            if address < 0 or address > self._ALL_ONES:
+                raise AddressValueError(address)
+            return
+
+        # Constructing from a packed address
+        if isinstance(address, Bytes):
+            try:
+                self._ip, = struct.unpack('!I', address)
+            except struct.error:
+                raise AddressValueError(address)  # Wrong length.
+            return
+
+        # Assume input argument to be string or any object representation
+        # which converts into a formatted IP string.
+        addr_str = str(address)
+        self._ip = self._ip_int_from_string(addr_str)
+
+
+class IPv4Network(_BaseV4, _BaseNet):
+
+    """This class represents and manipulates 32-bit IPv4 networks.
+
+    Attributes: [examples for IPv4Network('1.2.3.4/27')]
+        ._ip: 16909060
+        .ip: IPv4Address('1.2.3.4')
+        .network: IPv4Address('1.2.3.0')
+        .hostmask: IPv4Address('0.0.0.31')
+        .broadcast: IPv4Address('1.2.3.31')
+        .netmask: IPv4Address('255.255.255.224')
+        .prefixlen: 27
+
+    """
+
+    # the valid octets for host and netmasks. only useful for IPv4.
+    _valid_mask_octets = set((255, 254, 252, 248, 240, 224, 192, 128, 0))
+
+    def __init__(self, address, strict=False):
+        """Instantiate a new IPv4 network object.
+
+        Args:
+            address: A string or integer representing the IP [& network].
+              '192.168.1.1/24'
+              '192.168.1.1/255.255.255.0'
+              '192.168.1.1/0.0.0.255'
+              are all functionally the same in IPv4. Similarly,
+              '192.168.1.1'
+              '192.168.1.1/255.255.255.255'
+              '192.168.1.1/32'
+              are also functionaly equivalent. That is to say, failing to
+              provide a subnetmask will create an object with a mask of /32.
+
+              If the mask (portion after the / in the argument) is given in
+              dotted quad form, it is treated as a netmask if it starts with a
+              non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it
+              starts with a zero field (e.g. 0.255.255.255 == /8), with the
+              single exception of an all-zero mask which is treated as a
+              netmask == /0. If no mask is given, a default of /32 is used.
+
+              Additionally, an integer can be passed, so
+              IPv4Network('192.168.1.1') == IPv4Network(3232235777).
+              or, more generally
+              IPv4Network(int(IPv4Network('192.168.1.1'))) ==
+                IPv4Network('192.168.1.1')
+
+            strict: A boolean. If true, ensure that we have been passed
+              A true network address, eg, 192.168.1.0/24 and not an
+              IP address on a network, eg, 192.168.1.1/24.
+
+        Raises:
+            AddressValueError: If ipaddr isn't a valid IPv4 address.
+            NetmaskValueError: If the netmask isn't valid for
+              an IPv4 address.
+            ValueError: If strict was True and a network address was not
+              supplied.
+
+        """
+        _BaseNet.__init__(self, address)
+        _BaseV4.__init__(self, address)
+
+        # Constructing from an integer or packed bytes.
+        if isinstance(address, (int, long, Bytes)):
+            self.ip = IPv4Address(address)
+            self._ip = self.ip._ip
+            self._prefixlen = self._max_prefixlen
+            self.netmask = IPv4Address(self._ALL_ONES)
+            return
+
+        # Assume input argument to be string or any object representation
+        # which converts into a formatted IP prefix string.
+        addr = str(address).split('/')
+
+        if len(addr) > 2:
+            raise AddressValueError(address)
+
+        self._ip = self._ip_int_from_string(addr[0])
+        self.ip = IPv4Address(self._ip)
+
+        if len(addr) == 2:
+            mask = addr[1].split('.')
+            if len(mask) == 4:
+                # We have dotted decimal netmask.
+                if self._is_valid_netmask(addr[1]):
+                    self.netmask = IPv4Address(self._ip_int_from_string(
+                            addr[1]))
+                elif self._is_hostmask(addr[1]):
+                    self.netmask = IPv4Address(
+                        self._ip_int_from_string(addr[1]) ^ self._ALL_ONES)
+                else:
+                    raise NetmaskValueError('%s is not a valid netmask'
+                                                     % addr[1])
+
+                self._prefixlen = self._prefix_from_ip_int(int(self.netmask))
+            else:
+                # We have a netmask in prefix length form.
+                if not self._is_valid_netmask(addr[1]):
+                    raise NetmaskValueError(addr[1])
+                self._prefixlen = int(addr[1])
+                self.netmask = IPv4Address(self._ip_int_from_prefix(
+                    self._prefixlen))
+        else:
+            self._prefixlen = self._max_prefixlen
+            self.netmask = IPv4Address(self._ip_int_from_prefix(
+                self._prefixlen))
+        if strict:
+            if self.ip != self.network:
+                raise ValueError('%s has host bits set' %
+                                 self.ip)
+        if self._prefixlen == (self._max_prefixlen - 1):
+            self.iterhosts = self.__iter__
+
+    def _is_hostmask(self, ip_str):
+        """Test if the IP string is a hostmask (rather than a netmask).
+
+        Args:
+            ip_str: A string, the potential hostmask.
+
+        Returns:
+            A boolean, True if the IP string is a hostmask.
+
+        """
+        bits = ip_str.split('.')
+        try:
+            parts = [int(x) for x in bits if int(x) in self._valid_mask_octets]
+        except ValueError:
+            return False
+        if len(parts) != len(bits):
+            return False
+        if parts[0] < parts[-1]:
+            return True
+        return False
+
+    def _is_valid_netmask(self, netmask):
+        """Verify that the netmask is valid.
+
+        Args:
+            netmask: A string, either a prefix or dotted decimal
+              netmask.
+
+        Returns:
+            A boolean, True if the prefix represents a valid IPv4
+            netmask.
+
+        """
+        mask = netmask.split('.')
+        if len(mask) == 4:
+            if [x for x in mask if int(x) not in self._valid_mask_octets]:
+                return False
+            if [y for idx, y in enumerate(mask) if idx > 0 and
+                y > mask[idx - 1]]:
+                return False
+            return True
+        try:
+            netmask = int(netmask)
+        except ValueError:
+            return False
+        return 0 <= netmask <= self._max_prefixlen
+
+    # backwards compatibility
+    IsRFC1918 = lambda self: self.is_private
+    IsMulticast = lambda self: self.is_multicast
+    IsLoopback = lambda self: self.is_loopback
+    IsLinkLocal = lambda self: self.is_link_local
+
+
+class _BaseV6(object):
+
+    """Base IPv6 object.
+
+    The following methods are used by IPv6 objects in both single IP
+    addresses and networks.
+
+    """
+
+    _ALL_ONES = (2**IPV6LENGTH) - 1
+    _HEXTET_COUNT = 8
+    _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef')
+
+    def __init__(self, address):
+        self._version = 6
+        self._max_prefixlen = IPV6LENGTH
+
+    def _ip_int_from_string(self, ip_str):
+        """Turn an IPv6 ip_str into an integer.
+
+        Args:
+            ip_str: A string, the IPv6 ip_str.
+
+        Returns:
+            A long, the IPv6 ip_str.
+
+        Raises:
+            AddressValueError: if ip_str isn't a valid IPv6 Address.
+
+        """
+        parts = ip_str.split(':')
+
+        # An IPv6 address needs at least 2 colons (3 parts).
+        if len(parts) < 3:
+            raise AddressValueError(ip_str)
+
+        # If the address has an IPv4-style suffix, convert it to hexadecimal.
+        if '.' in parts[-1]:
+            ipv4_int = IPv4Address(parts.pop())._ip
+            parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF))
+            parts.append('%x' % (ipv4_int & 0xFFFF))
+
+        # An IPv6 address can't have more than 8 colons (9 parts).
+        if len(parts) > self._HEXTET_COUNT + 1:
+            raise AddressValueError(ip_str)
+
+        # Disregarding the endpoints, find '::' with nothing in between.
+        # This indicates that a run of zeroes has been skipped.
+        try:
+            skip_index, = (
+                [i for i in xrange(1, len(parts) - 1) if not parts[i]] or
+                [None])
+        except ValueError:
+            # Can't have more than one '::'
+            raise AddressValueError(ip_str)
+
+        # parts_hi is the number of parts to copy from above/before the '::'
+        # parts_lo is the number of parts to copy from below/after the '::'
+        if skip_index is not None:
+            # If we found a '::', then check if it also covers the endpoints.
+            parts_hi = skip_index
+            parts_lo = len(parts) - skip_index - 1
+            if not parts[0]:
+                parts_hi -= 1
+                if parts_hi:
+                    raise AddressValueError(ip_str)  # ^: requires ^::
+            if not parts[-1]:
+                parts_lo -= 1
+                if parts_lo:
+                    raise AddressValueError(ip_str)  # :$ requires ::$
+            parts_skipped = self._HEXTET_COUNT - (parts_hi + parts_lo)
+            if parts_skipped < 1:
+                raise AddressValueError(ip_str)
+        else:
+            # Otherwise, allocate the entire address to parts_hi.  The endpoints
+            # could still be empty, but _parse_hextet() will check for that.
+            if len(parts) != self._HEXTET_COUNT:
+                raise AddressValueError(ip_str)
+            parts_hi = len(parts)
+            parts_lo = 0
+            parts_skipped = 0
+
+        try:
+            # Now, parse the hextets into a 128-bit integer.
+            ip_int = 0L
+            for i in xrange(parts_hi):
+                ip_int <<= 16
+                ip_int |= self._parse_hextet(parts[i])
+            ip_int <<= 16 * parts_skipped
+            for i in xrange(-parts_lo, 0):
+                ip_int <<= 16
+                ip_int |= self._parse_hextet(parts[i])
+            return ip_int
+        except ValueError:
+            raise AddressValueError(ip_str)
+
+    def _parse_hextet(self, hextet_str):
+        """Convert an IPv6 hextet string into an integer.
+
+        Args:
+            hextet_str: A string, the number to parse.
+
+        Returns:
+            The hextet as an integer.
+
+        Raises:
+            ValueError: if the input isn't strictly a hex number from [0..FFFF].
+
+        """
+        # Whitelist the characters, since int() allows a lot of bizarre stuff.
+        if not self._HEX_DIGITS.issuperset(hextet_str):
+            raise ValueError
+        hextet_int = int(hextet_str, 16)
+        if hextet_int > 0xFFFF:
+            raise ValueError
+        return hextet_int
+
+    def _compress_hextets(self, hextets):
+        """Compresses a list of hextets.
+
+        Compresses a list of strings, replacing the longest continuous
+        sequence of "0" in the list with "" and adding empty strings at
+        the beginning or at the end of the string such that subsequently
+        calling ":".join(hextets) will produce the compressed version of
+        the IPv6 address.
+
+        Args:
+            hextets: A list of strings, the hextets to compress.
+
+        Returns:
+            A list of strings.
+
+        """
+        best_doublecolon_start = -1
+        best_doublecolon_len = 0
+        doublecolon_start = -1
+        doublecolon_len = 0
+        for index in range(len(hextets)):
+            if hextets[index] == '0':
+                doublecolon_len += 1
+                if doublecolon_start == -1:
+                    # Start of a sequence of zeros.
+                    doublecolon_start = index
+                if doublecolon_len > best_doublecolon_len:
+                    # This is the longest sequence of zeros so far.
+                    best_doublecolon_len = doublecolon_len
+                    best_doublecolon_start = doublecolon_start
+            else:
+                doublecolon_len = 0
+                doublecolon_start = -1
+
+        if best_doublecolon_len > 1:
+            best_doublecolon_end = (best_doublecolon_start +
+                                    best_doublecolon_len)
+            # For zeros at the end of the address.
+            if best_doublecolon_end == len(hextets):
+                hextets += ['']
+            hextets[best_doublecolon_start:best_doublecolon_end] = ['']
+            # For zeros at the beginning of the address.
+            if best_doublecolon_start == 0:
+                hextets = [''] + hextets
+
+        return hextets
+
+    def _string_from_ip_int(self, ip_int=None):
+        """Turns a 128-bit integer into hexadecimal notation.
+
+        Args:
+            ip_int: An integer, the IP address.
+
+        Returns:
+            A string, the hexadecimal representation of the address.
+
+        Raises:
+            ValueError: The address is bigger than 128 bits of all ones.
+
+        """
+        if not ip_int and ip_int != 0:
+            ip_int = int(self._ip)
+
+        if ip_int > self._ALL_ONES:
+            raise ValueError('IPv6 address is too large')
+
+        hex_str = '%032x' % ip_int
+        hextets = []
+        for x in range(0, 32, 4):
+            hextets.append('%x' % int(hex_str[x:x+4], 16))
+
+        hextets = self._compress_hextets(hextets)
+        return ':'.join(hextets)
+
+    def _explode_shorthand_ip_string(self):
+        """Expand a shortened IPv6 address.
+
+        Args:
+            ip_str: A string, the IPv6 address.
+
+        Returns:
+            A string, the expanded IPv6 address.
+
+        """
+        if isinstance(self, _BaseNet):
+            ip_str = str(self.ip)
+        else:
+            ip_str = str(self)
+
+        ip_int = self._ip_int_from_string(ip_str)
+        parts = []
+        for i in xrange(self._HEXTET_COUNT):
+            parts.append('%04x' % (ip_int & 0xFFFF))
+            ip_int >>= 16
+        parts.reverse()
+        if isinstance(self, _BaseNet):
+            return '%s/%d' % (':'.join(parts), self.prefixlen)
+        return ':'.join(parts)
+
+    @property
+    def max_prefixlen(self):
+        return self._max_prefixlen
+
+    @property
+    def packed(self):
+        """The binary representation of this address."""
+        return v6_int_to_packed(self._ip)
+
+    @property
+    def version(self):
+        return self._version
+
+    @property
+    def is_multicast(self):
+        """Test if the address is reserved for multicast use.
+
+        Returns:
+            A boolean, True if the address is a multicast address.
+            See RFC 2373 2.7 for details.
+
+        """
+        return self in IPv6Network('ff00::/8')
+
+    @property
+    def is_reserved(self):
+        """Test if the address is otherwise IETF reserved.
+
+        Returns:
+            A boolean, True if the address is within one of the
+            reserved IPv6 Network ranges.
+
+        """
+        return (self in IPv6Network('::/8') or
+                self in IPv6Network('100::/8') or
+                self in IPv6Network('200::/7') or
+                self in IPv6Network('400::/6') or
+                self in IPv6Network('800::/5') or
+                self in IPv6Network('1000::/4') or
+                self in IPv6Network('4000::/3') or
+                self in IPv6Network('6000::/3') or
+                self in IPv6Network('8000::/3') or
+                self in IPv6Network('A000::/3') or
+                self in IPv6Network('C000::/3') or
+                self in IPv6Network('E000::/4') or
+                self in IPv6Network('F000::/5') or
+                self in IPv6Network('F800::/6') or
+                self in IPv6Network('FE00::/9'))
+
+    @property
+    def is_unspecified(self):
+        """Test if the address is unspecified.
+
+        Returns:
+            A boolean, True if this is the unspecified address as defined in
+            RFC 2373 2.5.2.
+
+        """
+        return self._ip == 0 and getattr(self, '_prefixlen', 128) == 128
+
+    @property
+    def is_loopback(self):
+        """Test if the address is a loopback address.
+
+        Returns:
+            A boolean, True if the address is a loopback address as defined in
+            RFC 2373 2.5.3.
+
+        """
+        return self._ip == 1 and getattr(self, '_prefixlen', 128) == 128
+
+    @property
+    def is_link_local(self):
+        """Test if the address is reserved for link-local.
+
+        Returns:
+            A boolean, True if the address is reserved per RFC 4291.
+
+        """
+        return self in IPv6Network('fe80::/10')
+
+    @property
+    def is_site_local(self):
+        """Test if the address is reserved for site-local.
+
+        Note that the site-local address space has been deprecated by RFC 3879.
+        Use is_private to test if this address is in the space of unique local
+        addresses as defined by RFC 4193.
+
+        Returns:
+            A boolean, True if the address is reserved per RFC 3513 2.5.6.
+
+        """
+        return self in IPv6Network('fec0::/10')
+
+    @property
+    def is_private(self):
+        """Test if this address is allocated for private networks.
+
+        Returns:
+            A boolean, True if the address is reserved per RFC 4193.
+
+        """
+        return self in IPv6Network('fc00::/7')
+
+    @property
+    def ipv4_mapped(self):
+        """Return the IPv4 mapped address.
+
+        Returns:
+            If the IPv6 address is a v4 mapped address, return the
+            IPv4 mapped address. Return None otherwise.
+
+        """
+        if (self._ip >> 32) != 0xFFFF:
+            return None
+        return IPv4Address(self._ip & 0xFFFFFFFF)
+
+    @property
+    def teredo(self):
+        """Tuple of embedded teredo IPs.
+
+        Returns:
+            Tuple of the (server, client) IPs or None if the address
+            doesn't appear to be a teredo address (doesn't start with
+            2001::/32)
+
+        """
+        if (self._ip >> 96) != 0x20010000:
+            return None
+        return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF),
+                IPv4Address(~self._ip & 0xFFFFFFFF))
+
+    @property
+    def sixtofour(self):
+        """Return the IPv4 6to4 embedded address.
+
+        Returns:
+            The IPv4 6to4-embedded address if present or None if the
+            address doesn't appear to contain a 6to4 embedded address.
+
+        """
+        if (self._ip >> 112) != 0x2002:
+            return None
+        return IPv4Address((self._ip >> 80) & 0xFFFFFFFF)
+
+
+class IPv6Address(_BaseV6, _BaseIP):
+
+    """Represent and manipulate single IPv6 Addresses.
+    """
+
+    def __init__(self, address):
+        """Instantiate a new IPv6 address object.
+
+        Args:
+            address: A string or integer representing the IP
+
+              Additionally, an integer can be passed, so
+              IPv6Address('2001:4860::') ==
+                IPv6Address(42541956101370907050197289607612071936L).
+              or, more generally
+              IPv6Address(IPv6Address('2001:4860::')._ip) ==
+                IPv6Address('2001:4860::')
+
+        Raises:
+            AddressValueError: If address isn't a valid IPv6 address.
+
+        """
+        _BaseV6.__init__(self, address)
+
+        # Efficient constructor from integer.
+        if isinstance(address, (int, long)):
+            self._ip = address
+            if address < 0 or address > self._ALL_ONES:
+                raise AddressValueError(address)
+            return
+
+        # Constructing from a packed address
+        if isinstance(address, Bytes):
+            try:
+                hi, lo = struct.unpack('!QQ', address)
+            except struct.error:
+                raise AddressValueError(address)  # Wrong length.
+            self._ip = (hi << 64) | lo
+            return
+
+        # Assume input argument to be string or any object representation
+        # which converts into a formatted IP string.
+        addr_str = str(address)
+        if not addr_str:
+            raise AddressValueError('')
+
+        self._ip = self._ip_int_from_string(addr_str)
+
+
+class IPv6Network(_BaseV6, _BaseNet):
+
+    """This class represents and manipulates 128-bit IPv6 networks.
+
+    Attributes: [examples for IPv6('2001:658:22A:CAFE:200::1/64')]
+        .ip: IPv6Address('2001:658:22a:cafe:200::1')
+        .network: IPv6Address('2001:658:22a:cafe::')
+        .hostmask: IPv6Address('::ffff:ffff:ffff:ffff')
+        .broadcast: IPv6Address('2001:658:22a:cafe:ffff:ffff:ffff:ffff')
+        .netmask: IPv6Address('ffff:ffff:ffff:ffff::')
+        .prefixlen: 64
+
+    """
+
+
+    def __init__(self, address, strict=False):
+        """Instantiate a new IPv6 Network object.
+
+        Args:
+            address: A string or integer representing the IPv6 network or the IP
+              and prefix/netmask.
+              '2001:4860::/128'
+              '2001:4860:0000:0000:0000:0000:0000:0000/128'
+              '2001:4860::'
+              are all functionally the same in IPv6.  That is to say,
+              failing to provide a subnetmask will create an object with
+              a mask of /128.
+
+              Additionally, an integer can be passed, so
+              IPv6Network('2001:4860::') ==
+                IPv6Network(42541956101370907050197289607612071936L).
+              or, more generally
+              IPv6Network(IPv6Network('2001:4860::')._ip) ==
+                IPv6Network('2001:4860::')
+
+            strict: A boolean. If true, ensure that we have been passed
+              A true network address, eg, 192.168.1.0/24 and not an
+              IP address on a network, eg, 192.168.1.1/24.
+
+        Raises:
+            AddressValueError: If address isn't a valid IPv6 address.
+            NetmaskValueError: If the netmask isn't valid for
+              an IPv6 address.
+            ValueError: If strict was True and a network address was not
+              supplied.
+
+        """
+        _BaseNet.__init__(self, address)
+        _BaseV6.__init__(self, address)
+
+        # Constructing from an integer or packed bytes.
+        if isinstance(address, (int, long, Bytes)):
+            self.ip = IPv6Address(address)
+            self._ip = self.ip._ip
+            self._prefixlen = self._max_prefixlen
+            self.netmask = IPv6Address(self._ALL_ONES)
+            return
+
+        # Assume input argument to be string or any object representation
+        # which converts into a formatted IP prefix string.
+        addr = str(address).split('/')
+
+        if len(addr) > 2:
+            raise AddressValueError(address)
+
+        self._ip = self._ip_int_from_string(addr[0])
+        self.ip = IPv6Address(self._ip)
+
+        if len(addr) == 2:
+            if self._is_valid_netmask(addr[1]):
+                self._prefixlen = int(addr[1])
+            else:
+                raise NetmaskValueError(addr[1])
+        else:
+            self._prefixlen = self._max_prefixlen
+
+        self.netmask = IPv6Address(self._ip_int_from_prefix(self._prefixlen))
+
+        if strict:
+            if self.ip != self.network:
+                raise ValueError('%s has host bits set' %
+                                 self.ip)
+        if self._prefixlen == (self._max_prefixlen - 1):
+            self.iterhosts = self.__iter__
+
+    def _is_valid_netmask(self, prefixlen):
+        """Verify that the netmask/prefixlen is valid.
+
+        Args:
+            prefixlen: A string, the netmask in prefix length format.
+
+        Returns:
+            A boolean, True if the prefix represents a valid IPv6
+            netmask.
+
+        """
+        try:
+            prefixlen = int(prefixlen)
+        except ValueError:
+            return False
+        return 0 <= prefixlen <= self._max_prefixlen
+
+    @property
+    def with_netmask(self):
+        return self.with_prefixlen

+ 221 - 0
plugins/Sidebar/maxminddb/reader.py

@@ -0,0 +1,221 @@
+"""
+maxminddb.reader
+~~~~~~~~~~~~~~~~
+
+This module contains the pure Python database reader and related classes.
+
+"""
+from __future__ import unicode_literals
+
+try:
+    import mmap
+except ImportError:
+    # pylint: disable=invalid-name
+    mmap = None
+
+import struct
+
+from maxminddb.compat import byte_from_int, int_from_byte, ipaddress
+from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY
+from maxminddb.decoder import Decoder
+from maxminddb.errors import InvalidDatabaseError
+from maxminddb.file import FileBuffer
+
+
+class Reader(object):
+
+    """
+    Instances of this class provide a reader for the MaxMind DB format. IP
+    addresses can be looked up using the ``get`` method.
+    """
+
+    _DATA_SECTION_SEPARATOR_SIZE = 16
+    _METADATA_START_MARKER = b"\xAB\xCD\xEFMaxMind.com"
+
+    _ipv4_start = None
+
+    def __init__(self, database, mode=MODE_AUTO):
+        """Reader for the MaxMind DB file format
+
+        Arguments:
+        database -- A path to a valid MaxMind DB file such as a GeoIP2
+                    database file.
+        mode -- mode to open the database with. Valid mode are:
+            * MODE_MMAP - read from memory map.
+            * MODE_FILE - read database as standard file.
+            * MODE_MEMORY - load database into memory.
+            * MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
+        """
+        if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
+            with open(database, 'rb') as db_file:
+                self._buffer = mmap.mmap(
+                    db_file.fileno(), 0, access=mmap.ACCESS_READ)
+                self._buffer_size = self._buffer.size()
+        elif mode in (MODE_AUTO, MODE_FILE):
+            self._buffer = FileBuffer(database)
+            self._buffer_size = self._buffer.size()
+        elif mode == MODE_MEMORY:
+            with open(database, 'rb') as db_file:
+                self._buffer = db_file.read()
+                self._buffer_size = len(self._buffer)
+        else:
+            raise ValueError('Unsupported open mode ({0}). Only MODE_AUTO, '
+                             ' MODE_FILE, and MODE_MEMORY are support by the pure Python '
+                             'Reader'.format(mode))
+
+        metadata_start = self._buffer.rfind(self._METADATA_START_MARKER,
+                                            max(0, self._buffer_size
+                                                - 128 * 1024))
+
+        if metadata_start == -1:
+            self.close()
+            raise InvalidDatabaseError('Error opening database file ({0}). '
+                                       'Is this a valid MaxMind DB file?'
+                                       ''.format(database))
+
+        metadata_start += len(self._METADATA_START_MARKER)
+        metadata_decoder = Decoder(self._buffer, metadata_start)
+        (metadata, _) = metadata_decoder.decode(metadata_start)
+        self._metadata = Metadata(
+            **metadata)  # pylint: disable=bad-option-value
+
+        self._decoder = Decoder(self._buffer, self._metadata.search_tree_size
+                                + self._DATA_SECTION_SEPARATOR_SIZE)
+
+    def metadata(self):
+        """Return the metadata associated with the MaxMind DB file"""
+        return self._metadata
+
+    def get(self, ip_address):
+        """Return the record for the ip_address in the MaxMind DB
+
+
+        Arguments:
+        ip_address -- an IP address in the standard string notation
+        """
+        address = ipaddress.ip_address(ip_address)
+
+        if address.version == 6 and self._metadata.ip_version == 4:
+            raise ValueError('Error looking up {0}. You attempted to look up '
+                             'an IPv6 address in an IPv4-only database.'.format(
+                                 ip_address))
+        pointer = self._find_address_in_tree(address)
+
+        return self._resolve_data_pointer(pointer) if pointer else None
+
+    def _find_address_in_tree(self, ip_address):
+        packed = ip_address.packed
+
+        bit_count = len(packed) * 8
+        node = self._start_node(bit_count)
+
+        for i in range(bit_count):
+            if node >= self._metadata.node_count:
+                break
+            bit = 1 & (int_from_byte(packed[i >> 3]) >> 7 - (i % 8))
+            node = self._read_node(node, bit)
+        if node == self._metadata.node_count:
+            # Record is empty
+            return 0
+        elif node > self._metadata.node_count:
+            return node
+
+        raise InvalidDatabaseError('Invalid node in search tree')
+
+    def _start_node(self, length):
+        if self._metadata.ip_version != 6 or length == 128:
+            return 0
+
+        # We are looking up an IPv4 address in an IPv6 tree. Skip over the
+        # first 96 nodes.
+        if self._ipv4_start:
+            return self._ipv4_start
+
+        node = 0
+        for _ in range(96):
+            if node >= self._metadata.node_count:
+                break
+            node = self._read_node(node, 0)
+        self._ipv4_start = node
+        return node
+
+    def _read_node(self, node_number, index):
+        base_offset = node_number * self._metadata.node_byte_size
+
+        record_size = self._metadata.record_size
+        if record_size == 24:
+            offset = base_offset + index * 3
+            node_bytes = b'\x00' + self._buffer[offset:offset + 3]
+        elif record_size == 28:
+            (middle,) = struct.unpack(
+                b'!B', self._buffer[base_offset + 3:base_offset + 4])
+            if index:
+                middle &= 0x0F
+            else:
+                middle = (0xF0 & middle) >> 4
+            offset = base_offset + index * 4
+            node_bytes = byte_from_int(
+                middle) + self._buffer[offset:offset + 3]
+        elif record_size == 32:
+            offset = base_offset + index * 4
+            node_bytes = self._buffer[offset:offset + 4]
+        else:
+            raise InvalidDatabaseError(
+                'Unknown record size: {0}'.format(record_size))
+        return struct.unpack(b'!I', node_bytes)[0]
+
+    def _resolve_data_pointer(self, pointer):
+        resolved = pointer - self._metadata.node_count + \
+            self._metadata.search_tree_size
+
+        if resolved > self._buffer_size:
+            raise InvalidDatabaseError(
+                "The MaxMind DB file's search tree is corrupt")
+
+        (data, _) = self._decoder.decode(resolved)
+        return data
+
+    def close(self):
+        """Closes the MaxMind DB file and returns the resources to the system"""
+        # pylint: disable=unidiomatic-typecheck
+        if type(self._buffer) not in (str, bytes):
+            self._buffer.close()
+
+
+class Metadata(object):
+
+    """Metadata for the MaxMind DB reader"""
+
+    # pylint: disable=too-many-instance-attributes
+    def __init__(self, **kwargs):
+        """Creates new Metadata object. kwargs are key/value pairs from spec"""
+        # Although I could just update __dict__, that is less obvious and it
+        # doesn't work well with static analysis tools and some IDEs
+        self.node_count = kwargs['node_count']
+        self.record_size = kwargs['record_size']
+        self.ip_version = kwargs['ip_version']
+        self.database_type = kwargs['database_type']
+        self.languages = kwargs['languages']
+        self.binary_format_major_version = kwargs[
+            'binary_format_major_version']
+        self.binary_format_minor_version = kwargs[
+            'binary_format_minor_version']
+        self.build_epoch = kwargs['build_epoch']
+        self.description = kwargs['description']
+
+    @property
+    def node_byte_size(self):
+        """The size of a node in bytes"""
+        return self.record_size // 4
+
+    @property
+    def search_tree_size(self):
+        """The size of the search tree"""
+        return self.node_count * self.node_byte_size
+
+    def __repr__(self):
+        args = ', '.join('%s=%r' % x for x in self.__dict__.items())
+        return '{module}.{class_name}({data})'.format(
+            module=self.__module__,
+            class_name=self.__class__.__name__,
+            data=args)

+ 60 - 0
plugins/Sidebar/media-globe/Detector.js

@@ -0,0 +1,60 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ * @author mr.doob / http://mrdoob.com/
+ */
+
+Detector = {
+
+  canvas : !! window.CanvasRenderingContext2D,
+  webgl : ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
+  workers : !! window.Worker,
+  fileapi : window.File && window.FileReader && window.FileList && window.Blob,
+
+  getWebGLErrorMessage : function () {
+
+    var domElement = document.createElement( 'div' );
+
+    domElement.style.fontFamily = 'monospace';
+    domElement.style.fontSize = '13px';
+    domElement.style.textAlign = 'center';
+    domElement.style.background = '#eee';
+    domElement.style.color = '#000';
+    domElement.style.padding = '1em';
+    domElement.style.width = '475px';
+    domElement.style.margin = '5em auto 0';
+
+    if ( ! this.webgl ) {
+
+      domElement.innerHTML = window.WebGLRenderingContext ? [
+        'Sorry, your graphics card doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a>'
+      ].join( '\n' ) : [
+        'Sorry, your browser doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a><br/>',
+        'Please try with',
+        '<a href="http://www.google.com/chrome">Chrome</a>, ',
+        '<a href="http://www.mozilla.com/en-US/firefox/new/">Firefox 4</a> or',
+        '<a href="http://nightly.webkit.org/">Webkit Nightly (Mac)</a>'
+      ].join( '\n' );
+
+    }
+
+    return domElement;
+
+  },
+
+  addGetWebGLMessage : function ( parameters ) {
+
+    var parent, id, domElement;
+
+    parameters = parameters || {};
+
+    parent = parameters.parent !== undefined ? parameters.parent : document.body;
+    id = parameters.id !== undefined ? parameters.id : 'oldie';
+
+    domElement = Detector.getWebGLErrorMessage();
+    domElement.id = id;
+
+    parent.appendChild( domElement );
+
+  }
+
+};

+ 12 - 0
plugins/Sidebar/media-globe/Tween.js

@@ -0,0 +1,12 @@
+// Tween.js - http://github.com/sole/tween.js
+var TWEEN=TWEEN||function(){var a,e,c,d,f=[];return{start:function(g){c=setInterval(this.update,1E3/(g||60))},stop:function(){clearInterval(c)},add:function(g){f.push(g)},remove:function(g){a=f.indexOf(g);a!==-1&&f.splice(a,1)},update:function(){a=0;e=f.length;for(d=(new Date).getTime();a<e;)if(f[a].update(d))a++;else{f.splice(a,1);e--}}}}();
+TWEEN.Tween=function(a){var e={},c={},d={},f=1E3,g=0,j=null,n=TWEEN.Easing.Linear.EaseNone,k=null,l=null,m=null;this.to=function(b,h){if(h!==null)f=h;for(var i in b)if(a[i]!==null)d[i]=b[i];return this};this.start=function(){TWEEN.add(this);j=(new Date).getTime()+g;for(var b in d)if(a[b]!==null){e[b]=a[b];c[b]=d[b]-a[b]}return this};this.stop=function(){TWEEN.remove(this);return this};this.delay=function(b){g=b;return this};this.easing=function(b){n=b;return this};this.chain=function(b){k=b};this.onUpdate=
+function(b){l=b;return this};this.onComplete=function(b){m=b;return this};this.update=function(b){var h,i;if(b<j)return true;b=(b-j)/f;b=b>1?1:b;i=n(b);for(h in c)a[h]=e[h]+c[h]*i;l!==null&&l.call(a,i);if(b==1){m!==null&&m.call(a);k!==null&&k.start();return false}return true}};TWEEN.Easing={Linear:{},Quadratic:{},Cubic:{},Quartic:{},Quintic:{},Sinusoidal:{},Exponential:{},Circular:{},Elastic:{},Back:{},Bounce:{}};TWEEN.Easing.Linear.EaseNone=function(a){return a};
+TWEEN.Easing.Quadratic.EaseIn=function(a){return a*a};TWEEN.Easing.Quadratic.EaseOut=function(a){return-a*(a-2)};TWEEN.Easing.Quadratic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a;return-0.5*(--a*(a-2)-1)};TWEEN.Easing.Cubic.EaseIn=function(a){return a*a*a};TWEEN.Easing.Cubic.EaseOut=function(a){return--a*a*a+1};TWEEN.Easing.Cubic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a;return 0.5*((a-=2)*a*a+2)};TWEEN.Easing.Quartic.EaseIn=function(a){return a*a*a*a};
+TWEEN.Easing.Quartic.EaseOut=function(a){return-(--a*a*a*a-1)};TWEEN.Easing.Quartic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a;return-0.5*((a-=2)*a*a*a-2)};TWEEN.Easing.Quintic.EaseIn=function(a){return a*a*a*a*a};TWEEN.Easing.Quintic.EaseOut=function(a){return(a-=1)*a*a*a*a+1};TWEEN.Easing.Quintic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a*a;return 0.5*((a-=2)*a*a*a*a+2)};TWEEN.Easing.Sinusoidal.EaseIn=function(a){return-Math.cos(a*Math.PI/2)+1};
+TWEEN.Easing.Sinusoidal.EaseOut=function(a){return Math.sin(a*Math.PI/2)};TWEEN.Easing.Sinusoidal.EaseInOut=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};TWEEN.Easing.Exponential.EaseIn=function(a){return a==0?0:Math.pow(2,10*(a-1))};TWEEN.Easing.Exponential.EaseOut=function(a){return a==1?1:-Math.pow(2,-10*a)+1};TWEEN.Easing.Exponential.EaseInOut=function(a){if(a==0)return 0;if(a==1)return 1;if((a*=2)<1)return 0.5*Math.pow(2,10*(a-1));return 0.5*(-Math.pow(2,-10*(a-1))+2)};
+TWEEN.Easing.Circular.EaseIn=function(a){return-(Math.sqrt(1-a*a)-1)};TWEEN.Easing.Circular.EaseOut=function(a){return Math.sqrt(1- --a*a)};TWEEN.Easing.Circular.EaseInOut=function(a){if((a/=0.5)<1)return-0.5*(Math.sqrt(1-a*a)-1);return 0.5*(Math.sqrt(1-(a-=2)*a)+1)};TWEEN.Easing.Elastic.EaseIn=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return-(c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d))};
+TWEEN.Easing.Elastic.EaseOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return c*Math.pow(2,-10*a)*Math.sin((a-e)*2*Math.PI/d)+1};
+TWEEN.Easing.Elastic.EaseInOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);if((a*=2)<1)return-0.5*c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d);return c*Math.pow(2,-10*(a-=1))*Math.sin((a-e)*2*Math.PI/d)*0.5+1};TWEEN.Easing.Back.EaseIn=function(a){return a*a*(2.70158*a-1.70158)};TWEEN.Easing.Back.EaseOut=function(a){return(a-=1)*a*(2.70158*a+1.70158)+1};
+TWEEN.Easing.Back.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*(3.5949095*a-2.5949095);return 0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)};TWEEN.Easing.Bounce.EaseIn=function(a){return 1-TWEEN.Easing.Bounce.EaseOut(1-a)};TWEEN.Easing.Bounce.EaseOut=function(a){return(a/=1)<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375};
+TWEEN.Easing.Bounce.EaseInOut=function(a){if(a<0.5)return TWEEN.Easing.Bounce.EaseIn(a*2)*0.5;return TWEEN.Easing.Bounce.EaseOut(a*2-1)*0.5+0.5};

File diff suppressed because it is too large
+ 891 - 0
plugins/Sidebar/media-globe/all.js


+ 424 - 0
plugins/Sidebar/media-globe/globe.js

@@ -0,0 +1,424 @@
+/**
+ * dat.globe Javascript WebGL Globe Toolkit
+ * http://dataarts.github.com/dat.globe
+ *
+ * Copyright 2011 Data Arts Team, Google Creative Lab
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+var DAT = DAT || {};
+
+DAT.Globe = function(container, opts) {
+  opts = opts || {};
+
+  var colorFn = opts.colorFn || function(x) {
+    var c = new THREE.Color();
+    c.setHSL( ( 0.5 - (x * 2) ), Math.max(0.8, 1.0 - (x * 3)), 0.5 );
+    return c;
+  };
+  var imgDir = opts.imgDir || '/globe/';
+
+  var Shaders = {
+    'earth' : {
+      uniforms: {
+        'texture': { type: 't', value: null }
+      },
+      vertexShader: [
+        'varying vec3 vNormal;',
+        'varying vec2 vUv;',
+        'void main() {',
+          'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+          'vNormal = normalize( normalMatrix * normal );',
+          'vUv = uv;',
+        '}'
+      ].join('\n'),
+      fragmentShader: [
+        'uniform sampler2D texture;',
+        'varying vec3 vNormal;',
+        'varying vec2 vUv;',
+        'void main() {',
+          'vec3 diffuse = texture2D( texture, vUv ).xyz;',
+          'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );',
+          'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );',
+          'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );',
+        '}'
+      ].join('\n')
+    },
+    'atmosphere' : {
+      uniforms: {},
+      vertexShader: [
+        'varying vec3 vNormal;',
+        'void main() {',
+          'vNormal = normalize( normalMatrix * normal );',
+          'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+        '}'
+      ].join('\n'),
+      fragmentShader: [
+        'varying vec3 vNormal;',
+        'void main() {',
+          'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );',
+          'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;',
+        '}'
+      ].join('\n')
+    }
+  };
+
+  var camera, scene, renderer, w, h;
+  var mesh, atmosphere, point, running;
+
+  var overRenderer;
+  var running = true;
+
+  var curZoomSpeed = 0;
+  var zoomSpeed = 50;
+
+  var mouse = { x: 0, y: 0 }, mouseOnDown = { x: 0, y: 0 };
+  var rotation = { x: 0, y: 0 },
+      target = { x: Math.PI*3/2, y: Math.PI / 6.0 },
+      targetOnDown = { x: 0, y: 0 };
+
+  var distance = 100000, distanceTarget = 100000;
+  var padding = 10;
+  var PI_HALF = Math.PI / 2;
+
+  function init() {
+
+    container.style.color = '#fff';
+    container.style.font = '13px/20px Arial, sans-serif';
+
+    var shader, uniforms, material;
+    w = container.offsetWidth || window.innerWidth;
+    h = container.offsetHeight || window.innerHeight;
+
+    camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000);
+    camera.position.z = distance;
+
+    scene = new THREE.Scene();
+
+    var geometry = new THREE.SphereGeometry(200, 40, 30);
+
+    shader = Shaders['earth'];
+    uniforms = THREE.UniformsUtils.clone(shader.uniforms);
+
+    uniforms['texture'].value = THREE.ImageUtils.loadTexture(imgDir+'world.jpg');
+
+    material = new THREE.ShaderMaterial({
+
+          uniforms: uniforms,
+          vertexShader: shader.vertexShader,
+          fragmentShader: shader.fragmentShader
+
+        });
+
+    mesh = new THREE.Mesh(geometry, material);
+    mesh.rotation.y = Math.PI;
+    scene.add(mesh);
+
+    shader = Shaders['atmosphere'];
+    uniforms = THREE.UniformsUtils.clone(shader.uniforms);
+
+    material = new THREE.ShaderMaterial({
+
+          uniforms: uniforms,
+          vertexShader: shader.vertexShader,
+          fragmentShader: shader.fragmentShader,
+          side: THREE.BackSide,
+          blending: THREE.AdditiveBlending,
+          transparent: true
+
+        });
+
+    mesh = new THREE.Mesh(geometry, material);
+    mesh.scale.set( 1.1, 1.1, 1.1 );
+    scene.add(mesh);
+
+    geometry = new THREE.BoxGeometry(2.75, 2.75, 1);
+    geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,0,-0.5));
+
+    point = new THREE.Mesh(geometry);
+
+    renderer = new THREE.WebGLRenderer({antialias: true});
+    renderer.setSize(w, h);
+    renderer.setClearColor( 0x212121, 1 );
+
+    renderer.domElement.style.position = 'relative';
+
+    container.appendChild(renderer.domElement);
+
+    container.addEventListener('mousedown', onMouseDown, false);
+
+    container.addEventListener('mousewheel', onMouseWheel, false);
+
+    document.addEventListener('keydown', onDocumentKeyDown, false);
+
+    window.addEventListener('resize', onWindowResize, false);
+
+    container.addEventListener('mouseover', function() {
+      overRenderer = true;
+    }, false);
+
+    container.addEventListener('mouseout', function() {
+      overRenderer = false;
+    }, false);
+  }
+
+  function addData(data, opts) {
+    var lat, lng, size, color, i, step, colorFnWrapper;
+
+    opts.animated = opts.animated || false;
+    this.is_animated = opts.animated;
+    opts.format = opts.format || 'magnitude'; // other option is 'legend'
+    if (opts.format === 'magnitude') {
+      step = 3;
+      colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
+    } else if (opts.format === 'legend') {
+      step = 4;
+      colorFnWrapper = function(data, i) { return colorFn(data[i+3]); }
+    } else if (opts.format === 'peer') {
+      colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
+    } else {
+      throw('error: format not supported: '+opts.format);
+    }
+
+    if (opts.animated) {
+      if (this._baseGeometry === undefined) {
+        this._baseGeometry = new THREE.Geometry();
+        for (i = 0; i < data.length; i += step) {
+          lat = data[i];
+          lng = data[i + 1];
+//        size = data[i + 2];
+          color = colorFnWrapper(data,i);
+          size = 0;
+          addPoint(lat, lng, size, color, this._baseGeometry);
+        }
+      }
+      if(this._morphTargetId === undefined) {
+        this._morphTargetId = 0;
+      } else {
+        this._morphTargetId += 1;
+      }
+      opts.name = opts.name || 'morphTarget'+this._morphTargetId;
+    }
+    var subgeo = new THREE.Geometry();
+    for (i = 0; i < data.length; i += step) {
+      lat = data[i];
+      lng = data[i + 1];
+      color = colorFnWrapper(data,i);
+      size = data[i + 2];
+      size = size*200;
+      addPoint(lat, lng, size, color, subgeo);
+    }
+    if (opts.animated) {
+      this._baseGeometry.morphTargets.push({'name': opts.name, vertices: subgeo.vertices});
+    } else {
+      this._baseGeometry = subgeo;
+    }
+
+  };
+
+  function createPoints() {
+    if (this._baseGeometry !== undefined) {
+      if (this.is_animated === false) {
+        this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
+              color: 0xffffff,
+              vertexColors: THREE.FaceColors,
+              morphTargets: false
+            }));
+      } else {
+        if (this._baseGeometry.morphTargets.length < 8) {
+          console.log('t l',this._baseGeometry.morphTargets.length);
+          var padding = 8-this._baseGeometry.morphTargets.length;
+          console.log('padding', padding);
+          for(var i=0; i<=padding; i++) {
+            console.log('padding',i);
+            this._baseGeometry.morphTargets.push({'name': 'morphPadding'+i, vertices: this._baseGeometry.vertices});
+          }
+        }
+        this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
+              color: 0xffffff,
+              vertexColors: THREE.FaceColors,
+              morphTargets: true
+            }));
+      }
+      scene.add(this.points);
+    }
+  }
+
+  function addPoint(lat, lng, size, color, subgeo) {
+
+    var phi = (90 - lat) * Math.PI / 180;
+    var theta = (180 - lng) * Math.PI / 180;
+
+    point.position.x = 200 * Math.sin(phi) * Math.cos(theta);
+    point.position.y = 200 * Math.cos(phi);
+    point.position.z = 200 * Math.sin(phi) * Math.sin(theta);
+
+    point.lookAt(mesh.position);
+
+    point.scale.z = Math.max( size, 0.1 ); // avoid non-invertible matrix
+    point.updateMatrix();
+
+    for (var i = 0; i < point.geometry.faces.length; i++) {
+
+      point.geometry.faces[i].color = color;
+
+    }
+    if(point.matrixAutoUpdate){
+      point.updateMatrix();
+    }
+    subgeo.merge(point.geometry, point.matrix);
+  }
+
+  function onMouseDown(event) {
+    event.preventDefault();
+
+    container.addEventListener('mousemove', onMouseMove, false);
+    container.addEventListener('mouseup', onMouseUp, false);
+    container.addEventListener('mouseout', onMouseOut, false);
+
+    mouseOnDown.x = - event.clientX;
+    mouseOnDown.y = event.clientY;
+
+    targetOnDown.x = target.x;
+    targetOnDown.y = target.y;
+
+    container.style.cursor = 'move';
+  }
+
+  function onMouseMove(event) {
+    mouse.x = - event.clientX;
+    mouse.y = event.clientY;
+
+    var zoomDamp = distance/1000;
+
+    target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
+    target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
+
+    target.y = target.y > PI_HALF ? PI_HALF : target.y;
+    target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
+  }
+
+  function onMouseUp(event) {
+    container.removeEventListener('mousemove', onMouseMove, false);
+    container.removeEventListener('mouseup', onMouseUp, false);
+    container.removeEventListener('mouseout', onMouseOut, false);
+    container.style.cursor = 'auto';
+  }
+
+  function onMouseOut(event) {
+    container.removeEventListener('mousemove', onMouseMove, false);
+    container.removeEventListener('mouseup', onMouseUp, false);
+    container.removeEventListener('mouseout', onMouseOut, false);
+  }
+
+  function onMouseWheel(event) {
+    event.preventDefault();
+    if (overRenderer) {
+      zoom(event.wheelDeltaY * 0.3);
+    }
+    return false;
+  }
+
+  function onDocumentKeyDown(event) {
+    switch (event.keyCode) {
+      case 38:
+        zoom(100);
+        event.preventDefault();
+        break;
+      case 40:
+        zoom(-100);
+        event.preventDefault();
+        break;
+    }
+  }
+
+  function onWindowResize( event ) {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize( container.offsetWidth, container.offsetHeight );
+  }
+
+  function zoom(delta) {
+    distanceTarget -= delta;
+    distanceTarget = distanceTarget > 855 ? 855 : distanceTarget;
+    distanceTarget = distanceTarget < 350 ? 350 : distanceTarget;
+  }
+
+  function animate() {
+    if (!running) return
+    requestAnimationFrame(animate);
+    render();
+  }
+
+  function render() {
+    zoom(curZoomSpeed);
+
+    rotation.x += (target.x - rotation.x) * 0.1;
+    rotation.y += (target.y - rotation.y) * 0.1;
+    distance += (distanceTarget - distance) * 0.3;
+
+    camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
+    camera.position.y = distance * Math.sin(rotation.y);
+    camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
+
+    camera.lookAt(mesh.position);
+
+    renderer.render(scene, camera);
+  }
+
+  function unload() {
+    running = false
+    container.removeEventListener('mousedown', onMouseDown, false);
+    container.removeEventListener('mousewheel', onMouseWheel, false);
+    document.removeEventListener('keydown', onDocumentKeyDown, false);
+    window.removeEventListener('resize', onWindowResize, false);
+
+  }
+
+  init();
+  this.animate = animate;
+  this.unload = unload;
+
+
+  this.__defineGetter__('time', function() {
+    return this._time || 0;
+  });
+
+  this.__defineSetter__('time', function(t) {
+    var validMorphs = [];
+    var morphDict = this.points.morphTargetDictionary;
+    for(var k in morphDict) {
+      if(k.indexOf('morphPadding') < 0) {
+        validMorphs.push(morphDict[k]);
+      }
+    }
+    validMorphs.sort();
+    var l = validMorphs.length-1;
+    var scaledt = t*l+1;
+    var index = Math.floor(scaledt);
+    for (i=0;i<validMorphs.length;i++) {
+      this.points.morphTargetInfluences[validMorphs[i]] = 0;
+    }
+    var lastIndex = index - 1;
+    var leftover = scaledt - index;
+    if (lastIndex >= 0) {
+      this.points.morphTargetInfluences[lastIndex] = 1 - leftover;
+    }
+    this.points.morphTargetInfluences[index] = leftover;
+    this._time = t;
+  });
+
+  this.addData = addData;
+  this.createPoints = createPoints;
+  this.renderer = renderer;
+  this.scene = scene;
+
+  return this;
+
+};
+

File diff suppressed because it is too large
+ 372 - 0
plugins/Sidebar/media-globe/three.min.js


BIN
plugins/Sidebar/media-globe/world.jpg


+ 23 - 0
plugins/Sidebar/media/Class.coffee

@@ -0,0 +1,23 @@
+class Class
+	trace: true
+
+	log: (args...) ->
+		return unless @trace
+		return if typeof console is 'undefined'
+		args.unshift("[#{@.constructor.name}]")
+		console.log(args...)
+		@
+		
+	logStart: (name, args...) ->
+		return unless @trace
+		@logtimers or= {}
+		@logtimers[name] = +(new Date)
+		@log "#{name}", args..., "(started)" if args.length > 0
+		@
+		
+	logEnd: (name, args...) ->
+		ms = +(new Date)-@logtimers[name]
+		@log "#{name}", args..., "(Done in #{ms}ms)"
+		@ 
+
+window.Class = Class

+ 89 - 0
plugins/Sidebar/media/Scrollable.js

@@ -0,0 +1,89 @@
+/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
+
+window.initScrollable = function () {
+
+    var scrollContainer = document.querySelector('.scrollable'),
+        scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
+        scrollContent = document.querySelector('.scrollable .content'),
+        contentPosition = 0,
+        scrollerBeingDragged = false,
+        scroller,
+        topPosition,
+        scrollerHeight;
+
+    function calculateScrollerHeight() {
+        // *Calculation of how tall scroller should be
+        var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
+        if (visibleRatio == 1)
+            scroller.style.display = "none"
+        else
+            scroller.style.display = "block"
+        return visibleRatio * scrollContainer.offsetHeight;
+    }
+
+    function moveScroller(evt) {
+        // Move Scroll bar to top offset
+        var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
+        topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
+        scroller.style.top = topPosition + 'px';
+    }
+
+    function startDrag(evt) {
+        normalizedPosition = evt.pageY;
+        contentPosition = scrollContentWrapper.scrollTop;
+        scrollerBeingDragged = true;
+        window.addEventListener('mousemove', scrollBarScroll)
+    }
+
+    function stopDrag(evt) {
+        scrollerBeingDragged = false;
+        window.removeEventListener('mousemove', scrollBarScroll)
+    }
+
+    function scrollBarScroll(evt) {
+        if (scrollerBeingDragged === true) {
+            var mouseDifferential = evt.pageY - normalizedPosition;
+            var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
+            scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
+        }
+    }
+
+    function updateHeight() {
+        scrollerHeight = calculateScrollerHeight()-10;
+        scroller.style.height = scrollerHeight + 'px';
+    }
+
+    function createScroller() {
+        // *Creates scroller element and appends to '.scrollable' div
+        // create scroller element
+        scroller = document.createElement("div");
+        scroller.className = 'scroller';
+
+        // determine how big scroller should be based on content
+        scrollerHeight = calculateScrollerHeight()-10;
+
+        if (scrollerHeight / scrollContainer.offsetHeight < 1){
+            // *If there is a need to have scroll bar based on content size
+            scroller.style.height = scrollerHeight + 'px';
+
+            // append scroller to scrollContainer div
+            scrollContainer.appendChild(scroller);
+
+            // show scroll path divot
+            scrollContainer.className += ' showScroll';
+
+            // attach related draggable listeners
+            scroller.addEventListener('mousedown', startDrag);
+            window.addEventListener('mouseup', stopDrag);
+        }
+
+    }
+
+    createScroller();
+
+
+    // *** Listeners ***
+    scrollContentWrapper.addEventListener('scroll', moveScroller);
+
+    return updateHeight
+};

+ 44 - 0
plugins/Sidebar/media/Scrollbable.css

@@ -0,0 +1,44 @@
+.scrollable {
+    overflow: hidden;
+}
+
+.scrollable.showScroll::after {
+    position: absolute;
+    content: '';
+    top: 5%;
+    right: 7px;
+    height: 90%;
+    width: 3px;
+    background: rgba(224, 224, 255, .3);
+}
+
+.scrollable .content-wrapper {
+    width: 100%;
+    height: 100%;
+    padding-right: 50%;
+    overflow-y: scroll;
+}
+.scroller {
+    margin-top: 5px;
+    z-index: 5;
+    cursor: pointer;
+    position: absolute;
+    width: 7px;
+    border-radius: 5px;
+    background: #151515;
+    top: 0px;
+    left: 395px;
+    -webkit-transition: top .08s;
+    -moz-transition: top .08s;
+    -ms-transition: top .08s;
+    -o-transition: top .08s;
+    transition: top .08s;
+}
+.content {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}

+ 318 - 0
plugins/Sidebar/media/Sidebar.coffee

@@ -0,0 +1,318 @@
+class Sidebar extends Class
+	constructor: ->
+		@tag = null
+		@container = null
+		@opened = false
+		@width = 410
+		@fixbutton = $(".fixbutton")
+		@fixbutton_addx = 0
+		@fixbutton_initx = 0
+		@fixbutton_targetx = 0
+		@frame = $("#inner-iframe")
+		@initFixbutton()
+		@dragStarted = 0
+		@globe = null
+
+		@original_set_site_info = wrapper.setSiteInfo  # We going to override this, save the original
+
+		# Start in opened state for debugging
+		if false
+			@startDrag()
+			@moved()
+			@fixbutton_targetx = @fixbutton_initx - @width
+			@stopDrag()
+
+
+	initFixbutton: ->
+		# Detect dragging
+		@fixbutton.on "mousedown", (e) =>
+			e.preventDefault()
+
+			# Disable previous listeners
+			@fixbutton.off "click"
+			@fixbutton.off "mousemove"
+
+			# Make sure its not a click
+			@dragStarted = (+ new Date)
+			@fixbutton.one "mousemove", (e) =>
+				@fixbutton_addx = @fixbutton.offset().left-e.pageX
+				@startDrag()
+		@fixbutton.parent().on "click", (e) =>
+			@stopDrag()
+		@fixbutton_initx = @fixbutton.offset().left  # Initial x position
+
+
+	# Start dragging the fixbutton
+	startDrag: ->
+		@log "startDrag"
+		@fixbutton_targetx = @fixbutton_initx  # Fallback x position
+
+		@fixbutton.addClass("dragging")
+
+		# Fullscreen drag bg to capture mouse events over iframe
+		$("<div class='drag-bg'></div>").appendTo(document.body)
+
+		# IE position wrap fix
+		if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
+			@fixbutton.css("pointer-events", "none")
+
+		# Don't go to homepage
+		@fixbutton.one "click", (e) =>
+			@stopDrag()
+			@fixbutton.removeClass("dragging")
+			if Math.abs(@fixbutton.offset().left - @fixbutton_initx) > 5
+				# If moved more than some pixel the button then don't go to homepage
+				e.preventDefault()
+
+		# Animate drag
+		@fixbutton.parents().on "mousemove", @animDrag
+		@fixbutton.parents().on "mousemove" ,@waitMove
+
+		# Stop dragging listener
+		@fixbutton.parents().on "mouseup", (e) =>
+			e.preventDefault()
+			@stopDrag()
+
+
+	# Wait for moving the fixbutton
+	waitMove: (e) =>
+		if Math.abs(@fixbutton.offset().left - @fixbutton_targetx) > 10 and (+ new Date)-@dragStarted > 100
+			@moved()
+			@fixbutton.parents().off "mousemove" ,@waitMove
+
+	moved: ->
+		@log "Moved"
+		@createHtmltag()
+		$(document.body).css("perspective", "1000px").addClass("body-sidebar")
+		$(window).off "resize"
+		$(window).on "resize", =>
+			$(document.body).css "height", $(window).height()
+			@scrollable()
+		$(window).trigger "resize"
+
+		# Override setsiteinfo to catch changes
+		wrapper.setSiteInfo = (site_info) =>
+			@setSiteInfo(site_info)
+			@original_set_site_info.apply(wrapper, arguments)
+
+	setSiteInfo: (site_info) ->
+		@updateHtmlTag()
+		@displayGlobe()
+
+
+	# Create the sidebar html tag
+	createHtmltag: ->
+		if not @container
+			@container = $("""
+			<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
+			</div></div></div></div>
+			""")
+			@container.appendTo(document.body)
+			@tag = @container.find(".sidebar")
+			@updateHtmlTag()
+			@scrollable = window.initScrollable()
+
+
+	updateHtmlTag: ->
+		wrapper.ws.cmd "sidebarGetHtmlTag", {}, (res) =>
+			if @tag.find(".content").children().length == 0 # First update
+				@log "Creating content"
+				morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
+				@scrollable()
+
+			else  # Not first update, patch the html to keep unchanged dom elements
+				@log "Patching content"
+				morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
+					onBeforeMorphEl: (from_el, to_el) ->  # Ignore globe loaded state
+						if from_el.className == "globe"
+							return false
+						else
+							return true
+				}
+
+
+	animDrag: (e) =>
+		mousex = e.pageX
+
+		overdrag = @fixbutton_initx-@width-mousex
+		if overdrag > 0  # Overdragged
+			overdrag_percent = 1+overdrag/300
+			mousex = (e.pageX + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
+		targetx = @fixbutton_initx-mousex-@fixbutton_addx
+
+		@fixbutton.offset
+			left: mousex+@fixbutton_addx
+
+		if @tag
+			@tag.css("transform", "translateX(#{0-targetx}px)")
+
+		# Check if opened
+		if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
+			@fixbutton_targetx = @fixbutton_initx - @width  # Make it opened
+		else
+			@fixbutton_targetx = @fixbutton_initx
+
+
+	# Stop dragging the fixbutton
+	stopDrag: ->
+		@fixbutton.parents().off "mousemove"
+		@fixbutton.off "mousemove"
+		@fixbutton.css("pointer-events", "")
+		$(".drag-bg").remove()
+		if not @fixbutton.hasClass("dragging")
+			return
+		@fixbutton.removeClass("dragging")
+
+		# Move back to initial position
+		if @fixbutton_targetx != @fixbutton.offset().left
+			# Animate fixbutton
+			@fixbutton.stop().animate {"left": @fixbutton_targetx}, 500, "easeOutBack", =>
+				# Switch back to auto align
+				if @fixbutton_targetx == @fixbutton_initx  # Closed
+					@fixbutton.css("left", "auto")
+				else  # Opened
+					@fixbutton.css("left", @fixbutton_targetx)
+
+				$(".fixbutton-bg").trigger "mouseout"  # Switch fixbutton back to normal status
+
+			# Animate sidebar and iframe
+			if @fixbutton_targetx == @fixbutton_initx
+				# Closed
+				targetx = 0
+				@opened = false
+			else
+				# Opened
+				targetx = @width
+				if not @opened
+					@onOpened()
+				@opened = true
+
+			# Revent sidebar transitions
+			@tag.css("transition", "0.4s ease-out")
+			@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
+				@tag.css("transition", "")
+				if not @opened
+					@container.remove()
+					@container = null
+					@tag.remove()
+					@tag = null
+
+			# Revert body transformations
+			@log "stopdrag", "opened:", @opened
+			if not @opened
+				@onClosed()
+
+
+	onOpened: ->
+		@log "Opened"
+		@scrollable()
+
+		# Re-calculate height when site admin opened or closed
+		@tag.find("#checkbox-owned").off("click").on "click", =>
+			setTimeout (=>
+				@scrollable()
+			), 300
+
+		# Site limit button
+		@tag.find("#button-sitelimit").on "click", =>
+			wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), =>
+				wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
+				@updateHtmlTag()
+			return false
+
+		# Change identity button
+		@tag.find("#button-identity").on "click", =>
+			wrapper.ws.cmd "certSelect"
+			return false
+
+		# Owned checkbox
+		@tag.find("#checkbox-owned").on "click", =>
+			wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
+
+		# Save settings
+		@tag.find("#button-settings").on "click", =>
+			wrapper.ws.cmd "fileGet", "content.json", (res) =>
+				data = JSON.parse(res)
+				data["title"] = $("#settings-title").val()
+				data["description"] = $("#settings-description").val()
+				json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
+				wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw)], (res) =>
+					if res != "ok" # fileWrite failed
+						wrapper.notifications.add "file-write", "error", "File write error: #{res}"
+					else
+						wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
+						@updateHtmlTag()
+			return false
+
+		# Sign content.json
+		@tag.find("#button-sign").on "click", =>
+			inner_path = @tag.find("#select-contents").val()
+
+			if wrapper.site_info.privatekey
+				# Privatekey stored in users.json
+				wrapper.ws.cmd "siteSign", ["stored", inner_path], (res) =>
+					wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
+
+			else
+				# Ask the user for privatekey
+				wrapper.displayPrompt "Enter your private key:", "password", "Sign", (privatekey) => # Prompt the private key
+					wrapper.ws.cmd "siteSign", [privatekey, inner_path], (res) =>
+						if res == "ok"
+							wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
+
+			return false
+
+		# Publish content.json
+		@tag.find("#button-publish").on "click", =>
+			inner_path = @tag.find("#select-contents").val()
+			@tag.find("#button-publish").addClass "loading"
+			wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}, =>
+				@tag.find("#button-publish").removeClass "loading"
+
+		@loadGlobe()
+
+
+	onClosed: ->
+		$(window).off "resize"
+		$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
+			if e.target == document.body
+				$(document.body).css("height", "auto").css("perspective", "").css("transition", "").off transitionEnd
+				@unloadGlobe()
+
+		# We dont need site info anymore
+		wrapper.setSiteInfo = @original_set_site_info
+
+
+	loadGlobe: =>
+		if @tag.find(".globe").hasClass("loading")
+			setTimeout (=>
+				if typeof(DAT) == "undefined"  # Globe script not loaded, do it first
+					$.getScript("/uimedia/globe/all.js", @displayGlobe)
+				else
+					@displayGlobe()
+			), 600
+
+
+	displayGlobe: =>
+		wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
+			if @globe
+				@globe.scene.remove(@globe.points)
+				@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
+				@globe.createPoints()
+			else
+				@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
+				@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
+				@globe.createPoints()
+				@globe.animate()
+			@tag.find(".globe").removeClass("loading")
+
+
+	unloadGlobe: =>
+		if not @globe
+			return false
+		@globe.unload()
+		@globe = null
+
+
+window.sidebar = new Sidebar()
+window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'

+ 96 - 0
plugins/Sidebar/media/Sidebar.css

@@ -0,0 +1,96 @@
+.drag-bg { width: 100%; height: 100%; position: absolute; }
+.fixbutton.dragging { cursor: -webkit-grabbing; }
+.fixbutton-bg:active { cursor: -webkit-grabbing; }
+
+
+.body-sidebar { background-color: #666 !important;  }
+#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; backface-visibility: hidden; outline: 1px solid transparent }
+.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
+
+/* SIDEBAR */
+
+.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: absolute; }
+.sidebar { background-color: #212121; position: absolute; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
+.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200 }
+.sidebar h1, .sidebar h2 { font-weight: lighter; }
+.sidebar .button { margin: 0px; display: inline-block; }
+
+
+/* FIELDS */
+
+.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
+.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
+.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
+.sidebar .fields label { font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: block; margin-bottom: 10px; }
+.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
+.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; border-radius: 3px; width: 250px; font-family: Consolas, monospace; }
+.sidebar .fields .text.long { width: 330px; font-size: 72%; }
+.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
+.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
+.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
+
+/* Select */
+.sidebar .fields select {
+	width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; appearance: none;
+	padding: 5px; padding-right: 25px; border: 0px; border-radius: 3px; height: 35px; vertical-align: 1px; box-shadow: 0px 1px 2px rgba(0,0,0,0.5);
+}
+.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; transform: rotateZ(90deg); height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
+
+/* Checkbox */
+.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
+.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; margin-left: -59px; }
+.sidebar .fields .checkbox-skin:before {
+	content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
+	transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
+}
+.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
+.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
+
+/* Fake input */
+.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
+
+/* GRAPH */
+
+.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative;}
+.graph li { height: 100%; position: absolute; }
+.graph-stacked li { position: static; float: left; }
+
+.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
+.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
+.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
+.graph-legend span { position: absolute; }
+.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
+.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
+
+/* COLORS */
+
+.back-green { background-color: #2ECC71 }
+.color-green:before { color: #2ECC71 }
+.back-blue { background-color: #3BAFDA }
+.color-blue:before { color: #3BAFDA }
+.back-darkblue { background-color: #2196F3 }
+.color-darkblue:before { color: #2196F3 }
+.back-purple { background-color: #B10DC9 }
+.color-purple:before { color: #B10DC9 }
+.back-yellow { background-color: #FFDC00 }
+.color-yellow:before { color: #FFDC00 }
+.back-orange { background-color: #FF9800 }
+.color-orange:before { color: #FF9800 }
+.back-gray { background-color: #ECF0F1 }
+.color-gray:before { color: #ECF0F1 }
+.back-black { background-color: #34495E }
+.color-black:before { color: #34495E }
+.back-white { background-color: #EEE }
+.color-white:before { color: #EEE }
+
+
+/* Settings owned */
+
+.owned-title { float: left }
+#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
+#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden }
+#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
+
+/* Globe */
+.globe { width: 360px; height: 360px }
+.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }

+ 150 - 0
plugins/Sidebar/media/all.css

@@ -0,0 +1,150 @@
+
+
+/* ---- plugins/Sidebar/media/Scrollbable.css ---- */
+
+
+.scrollable {
+    overflow: hidden;
+}
+
+.scrollable.showScroll::after {
+    position: absolute;
+    content: '';
+    top: 5%;
+    right: 7px;
+    height: 90%;
+    width: 3px;
+    background: rgba(224, 224, 255, .3);
+}
+
+.scrollable .content-wrapper {
+    width: 100%;
+    height: 100%;
+    padding-right: 50%;
+    overflow-y: scroll;
+}
+.scroller {
+    margin-top: 5px;
+    z-index: 5;
+    cursor: pointer;
+    position: absolute;
+    width: 7px;
+    -webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ;
+    background: #151515;
+    top: 0px;
+    left: 395px;
+    -webkit-transition: top .08s;
+    -moz-transition: top .08s;
+    -ms-transition: top .08s;
+    -o-transition: top .08s;
+    -webkit-transition: top .08s; -moz-transition: top .08s; -o-transition: top .08s; -ms-transition: top .08s; transition: top .08s ;
+}
+.content {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+
+/* ---- plugins/Sidebar/media/Sidebar.css ---- */
+
+
+.drag-bg { width: 100%; height: 100%; position: absolute; }
+.fixbutton.dragging { cursor: -webkit-grabbing; }
+.fixbutton-bg:active { cursor: -webkit-grabbing; }
+
+
+.body-sidebar { background-color: #666 !important;  }
+#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; backface-visibility: hidden; outline: 1px solid transparent }
+.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
+
+/* SIDEBAR */
+
+.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: absolute; }
+.sidebar { background-color: #212121; position: absolute; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
+.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200 }
+.sidebar h1, .sidebar h2 { font-weight: lighter; }
+.sidebar .button { margin: 0px; display: inline-block; }
+
+
+/* FIELDS */
+
+.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
+.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
+.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
+.sidebar .fields label { font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: block; margin-bottom: 10px; }
+.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
+.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; width: 250px; font-family: Consolas, monospace; }
+.sidebar .fields .text.long { width: 330px; font-size: 72%; }
+.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
+.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
+.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
+
+/* Select */
+.sidebar .fields select {
+	width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; -ms-appearance: none; appearance: none ;
+	padding: 5px; padding-right: 25px; border: 0px; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; height: 35px; vertical-align: 1px; -webkit-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -moz-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -o-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -ms-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); box-shadow: 0px 1px 2px rgba(0,0,0,0.5) ;
+}
+.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; -webkit-transform: rotateZ(90deg); -moz-transform: rotateZ(90deg); -o-transform: rotateZ(90deg); -ms-transform: rotateZ(90deg); transform: rotateZ(90deg) ; height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
+
+/* Checkbox */
+.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
+.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; margin-left: -59px; }
+.sidebar .fields .checkbox-skin:before {
+	content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px;
+	-webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ;
+}
+.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
+.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
+
+/* Fake input */
+.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
+
+/* GRAPH */
+
+.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; -o-border-radius: 8px; -ms-border-radius: 8px; border-radius: 8px ; overflow: hidden; position: relative;}
+.graph li { height: 100%; position: absolute; }
+.graph-stacked li { position: static; float: left; }
+
+.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
+.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
+.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
+.graph-legend span { position: absolute; }
+.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
+.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
+
+/* COLORS */
+
+.back-green { background-color: #2ECC71 }
+.color-green:before { color: #2ECC71 }
+.back-blue { background-color: #3BAFDA }
+.color-blue:before { color: #3BAFDA }
+.back-darkblue { background-color: #2196F3 }
+.color-darkblue:before { color: #2196F3 }
+.back-purple { background-color: #B10DC9 }
+.color-purple:before { color: #B10DC9 }
+.back-yellow { background-color: #FFDC00 }
+.color-yellow:before { color: #FFDC00 }
+.back-orange { background-color: #FF9800 }
+.color-orange:before { color: #FF9800 }
+.back-gray { background-color: #ECF0F1 }
+.color-gray:before { color: #ECF0F1 }
+.back-black { background-color: #34495E }
+.color-black:before { color: #34495E }
+.back-white { background-color: #EEE }
+.color-white:before { color: #EEE }
+
+
+/* Settings owned */
+
+.owned-title { float: left }
+#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
+#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden }
+#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
+
+/* Globe */
+.globe { width: 360px; height: 360px }
+.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }

+ 882 - 0
plugins/Sidebar/media/all.js

@@ -0,0 +1,882 @@
+
+
+/* ---- plugins/Sidebar/media/Class.coffee ---- */
+
+
+(function() {
+  var Class,
+    __slice = [].slice;
+
+  Class = (function() {
+    function Class() {}
+
+    Class.prototype.trace = true;
+
+    Class.prototype.log = function() {
+      var args;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      if (!this.trace) {
+        return;
+      }
+      if (typeof console === 'undefined') {
+        return;
+      }
+      args.unshift("[" + this.constructor.name + "]");
+      console.log.apply(console, args);
+      return this;
+    };
+
+    Class.prototype.logStart = function() {
+      var args, name;
+      name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+      if (!this.trace) {
+        return;
+      }
+      this.logtimers || (this.logtimers = {});
+      this.logtimers[name] = +(new Date);
+      if (args.length > 0) {
+        this.log.apply(this, ["" + name].concat(__slice.call(args), ["(started)"]));
+      }
+      return this;
+    };
+
+    Class.prototype.logEnd = function() {
+      var args, ms, name;
+      name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+      ms = +(new Date) - this.logtimers[name];
+      this.log.apply(this, ["" + name].concat(__slice.call(args), ["(Done in " + ms + "ms)"]));
+      return this;
+    };
+
+    return Class;
+
+  })();
+
+  window.Class = Class;
+
+}).call(this);
+
+
+/* ---- plugins/Sidebar/media/Scrollable.js ---- */
+
+
+/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
+
+window.initScrollable = function () {
+
+    var scrollContainer = document.querySelector('.scrollable'),
+        scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
+        scrollContent = document.querySelector('.scrollable .content'),
+        contentPosition = 0,
+        scrollerBeingDragged = false,
+        scroller,
+        topPosition,
+        scrollerHeight;
+
+    function calculateScrollerHeight() {
+        // *Calculation of how tall scroller should be
+        var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
+        if (visibleRatio == 1)
+            scroller.style.display = "none"
+        else
+            scroller.style.display = "block"
+        return visibleRatio * scrollContainer.offsetHeight;
+    }
+
+    function moveScroller(evt) {
+        // Move Scroll bar to top offset
+        var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
+        topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
+        scroller.style.top = topPosition + 'px';
+    }
+
+    function startDrag(evt) {
+        normalizedPosition = evt.pageY;
+        contentPosition = scrollContentWrapper.scrollTop;
+        scrollerBeingDragged = true;
+        window.addEventListener('mousemove', scrollBarScroll)
+    }
+
+    function stopDrag(evt) {
+        scrollerBeingDragged = false;
+        window.removeEventListener('mousemove', scrollBarScroll)
+    }
+
+    function scrollBarScroll(evt) {
+        if (scrollerBeingDragged === true) {
+            var mouseDifferential = evt.pageY - normalizedPosition;
+            var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
+            scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
+        }
+    }
+
+    function updateHeight() {
+        scrollerHeight = calculateScrollerHeight()-10;
+        scroller.style.height = scrollerHeight + 'px';
+    }
+
+    function createScroller() {
+        // *Creates scroller element and appends to '.scrollable' div
+        // create scroller element
+        scroller = document.createElement("div");
+        scroller.className = 'scroller';
+
+        // determine how big scroller should be based on content
+        scrollerHeight = calculateScrollerHeight()-10;
+
+        if (scrollerHeight / scrollContainer.offsetHeight < 1){
+            // *If there is a need to have scroll bar based on content size
+            scroller.style.height = scrollerHeight + 'px';
+
+            // append scroller to scrollContainer div
+            scrollContainer.appendChild(scroller);
+
+            // show scroll path divot
+            scrollContainer.className += ' showScroll';
+
+            // attach related draggable listeners
+            scroller.addEventListener('mousedown', startDrag);
+            window.addEventListener('mouseup', stopDrag);
+        }
+
+    }
+
+    createScroller();
+
+
+    // *** Listeners ***
+    scrollContentWrapper.addEventListener('scroll', moveScroller);
+
+    return updateHeight
+};
+
+
+/* ---- plugins/Sidebar/media/Sidebar.coffee ---- */
+
+
+(function() {
+  var Sidebar,
+    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+    __hasProp = {}.hasOwnProperty;
+
+  Sidebar = (function(_super) {
+    __extends(Sidebar, _super);
+
+    function Sidebar() {
+      this.unloadGlobe = __bind(this.unloadGlobe, this);
+      this.displayGlobe = __bind(this.displayGlobe, this);
+      this.loadGlobe = __bind(this.loadGlobe, this);
+      this.animDrag = __bind(this.animDrag, this);
+      this.waitMove = __bind(this.waitMove, this);
+      this.tag = null;
+      this.container = null;
+      this.opened = false;
+      this.width = 410;
+      this.fixbutton = $(".fixbutton");
+      this.fixbutton_addx = 0;
+      this.fixbutton_initx = 0;
+      this.fixbutton_targetx = 0;
+      this.frame = $("#inner-iframe");
+      this.initFixbutton();
+      this.dragStarted = 0;
+      this.globe = null;
+      this.original_set_site_info = wrapper.setSiteInfo;
+      if (false) {
+        this.startDrag();
+        this.moved();
+        this.fixbutton_targetx = this.fixbutton_initx - this.width;
+        this.stopDrag();
+      }
+    }
+
+    Sidebar.prototype.initFixbutton = function() {
+      this.fixbutton.on("mousedown", (function(_this) {
+        return function(e) {
+          e.preventDefault();
+          _this.fixbutton.off("click");
+          _this.fixbutton.off("mousemove");
+          _this.dragStarted = +(new Date);
+          return _this.fixbutton.one("mousemove", function(e) {
+            _this.fixbutton_addx = _this.fixbutton.offset().left - e.pageX;
+            return _this.startDrag();
+          });
+        };
+      })(this));
+      this.fixbutton.parent().on("click", (function(_this) {
+        return function(e) {
+          return _this.stopDrag();
+        };
+      })(this));
+      return this.fixbutton_initx = this.fixbutton.offset().left;
+    };
+
+    Sidebar.prototype.startDrag = function() {
+      this.log("startDrag");
+      this.fixbutton_targetx = this.fixbutton_initx;
+      this.fixbutton.addClass("dragging");
+      $("<div class='drag-bg'></div>").appendTo(document.body);
+      if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
+        this.fixbutton.css("pointer-events", "none");
+      }
+      this.fixbutton.one("click", (function(_this) {
+        return function(e) {
+          _this.stopDrag();
+          _this.fixbutton.removeClass("dragging");
+          if (Math.abs(_this.fixbutton.offset().left - _this.fixbutton_initx) > 5) {
+            return e.preventDefault();
+          }
+        };
+      })(this));
+      this.fixbutton.parents().on("mousemove", this.animDrag);
+      this.fixbutton.parents().on("mousemove", this.waitMove);
+      return this.fixbutton.parents().on("mouseup", (function(_this) {
+        return function(e) {
+          e.preventDefault();
+          return _this.stopDrag();
+        };
+      })(this));
+    };
+
+    Sidebar.prototype.waitMove = function(e) {
+      if (Math.abs(this.fixbutton.offset().left - this.fixbutton_targetx) > 10 && (+(new Date)) - this.dragStarted > 100) {
+        this.moved();
+        return this.fixbutton.parents().off("mousemove", this.waitMove);
+      }
+    };
+
+    Sidebar.prototype.moved = function() {
+      this.log("Moved");
+      this.createHtmltag();
+      $(document.body).css("perspective", "1000px").addClass("body-sidebar");
+      $(window).off("resize");
+      $(window).on("resize", (function(_this) {
+        return function() {
+          $(document.body).css("height", $(window).height());
+          return _this.scrollable();
+        };
+      })(this));
+      $(window).trigger("resize");
+      return wrapper.setSiteInfo = (function(_this) {
+        return function(site_info) {
+          _this.setSiteInfo(site_info);
+          return _this.original_set_site_info.apply(wrapper, arguments);
+        };
+      })(this);
+    };
+
+    Sidebar.prototype.setSiteInfo = function(site_info) {
+      this.updateHtmlTag();
+      return this.displayGlobe();
+    };
+
+    Sidebar.prototype.createHtmltag = function() {
+      if (!this.container) {
+        this.container = $("<div class=\"sidebar-container\"><div class=\"sidebar scrollable\"><div class=\"content-wrapper\"><div class=\"content\">\n</div></div></div></div>");
+        this.container.appendTo(document.body);
+        this.tag = this.container.find(".sidebar");
+        this.updateHtmlTag();
+        return this.scrollable = window.initScrollable();
+      }
+    };
+
+    Sidebar.prototype.updateHtmlTag = function() {
+      return wrapper.ws.cmd("sidebarGetHtmlTag", {}, (function(_this) {
+        return function(res) {
+          if (_this.tag.find(".content").children().length === 0) {
+            _this.log("Creating content");
+            morphdom(_this.tag.find(".content")[0], '<div class="content">' + res + '</div>');
+            return _this.scrollable();
+          } else {
+            _this.log("Patching content");
+            return morphdom(_this.tag.find(".content")[0], '<div class="content">' + res + '</div>', {
+              onBeforeMorphEl: function(from_el, to_el) {
+                if (from_el.className === "globe") {
+                  return false;
+                } else {
+                  return true;
+                }
+              }
+            });
+          }
+        };
+      })(this));
+    };
+
+    Sidebar.prototype.animDrag = function(e) {
+      var mousex, overdrag, overdrag_percent, targetx;
+      mousex = e.pageX;
+      overdrag = this.fixbutton_initx - this.width - mousex;
+      if (overdrag > 0) {
+        overdrag_percent = 1 + overdrag / 300;
+        mousex = (e.pageX + (this.fixbutton_initx - this.width) * overdrag_percent) / (1 + overdrag_percent);
+      }
+      targetx = this.fixbutton_initx - mousex - this.fixbutton_addx;
+      this.fixbutton.offset({
+        left: mousex + this.fixbutton_addx
+      });
+      if (this.tag) {
+        this.tag.css("transform", "translateX(" + (0 - targetx) + "px)");
+      }
+      if ((!this.opened && targetx > this.width / 3) || (this.opened && targetx > this.width * 0.9)) {
+        return this.fixbutton_targetx = this.fixbutton_initx - this.width;
+      } else {
+        return this.fixbutton_targetx = this.fixbutton_initx;
+      }
+    };
+
+    Sidebar.prototype.stopDrag = function() {
+      var targetx;
+      this.fixbutton.parents().off("mousemove");
+      this.fixbutton.off("mousemove");
+      this.fixbutton.css("pointer-events", "");
+      $(".drag-bg").remove();
+      if (!this.fixbutton.hasClass("dragging")) {
+        return;
+      }
+      this.fixbutton.removeClass("dragging");
+      if (this.fixbutton_targetx !== this.fixbutton.offset().left) {
+        this.fixbutton.stop().animate({
+          "left": this.fixbutton_targetx
+        }, 500, "easeOutBack", (function(_this) {
+          return function() {
+            if (_this.fixbutton_targetx === _this.fixbutton_initx) {
+              _this.fixbutton.css("left", "auto");
+            } else {
+              _this.fixbutton.css("left", _this.fixbutton_targetx);
+            }
+            return $(".fixbutton-bg").trigger("mouseout");
+          };
+        })(this));
+        if (this.fixbutton_targetx === this.fixbutton_initx) {
+          targetx = 0;
+          this.opened = false;
+        } else {
+          targetx = this.width;
+          if (!this.opened) {
+            this.onOpened();
+          }
+          this.opened = true;
+        }
+        this.tag.css("transition", "0.4s ease-out");
+        this.tag.css("transform", "translateX(-" + targetx + "px)").one(transitionEnd, (function(_this) {
+          return function() {
+            _this.tag.css("transition", "");
+            if (!_this.opened) {
+              _this.container.remove();
+              _this.container = null;
+              _this.tag.remove();
+              return _this.tag = null;
+            }
+          };
+        })(this));
+        this.log("stopdrag", "opened:", this.opened);
+        if (!this.opened) {
+          return this.onClosed();
+        }
+      }
+    };
+
+    Sidebar.prototype.onOpened = function() {
+      this.log("Opened");
+      this.scrollable();
+      this.tag.find("#checkbox-owned").off("click").on("click", (function(_this) {
+        return function() {
+          return setTimeout((function() {
+            return _this.scrollable();
+          }), 300);
+        };
+      })(this));
+      this.tag.find("#button-sitelimit").on("click", (function(_this) {
+        return function() {
+          wrapper.ws.cmd("siteSetLimit", $("#input-sitelimit").val(), function() {
+            wrapper.notifications.add("done-sitelimit", "done", "Site storage limit modified!", 5000);
+            return _this.updateHtmlTag();
+          });
+          return false;
+        };
+      })(this));
+      this.tag.find("#button-identity").on("click", (function(_this) {
+        return function() {
+          wrapper.ws.cmd("certSelect");
+          return false;
+        };
+      })(this));
+      this.tag.find("#checkbox-owned").on("click", (function(_this) {
+        return function() {
+          return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]);
+        };
+      })(this));
+      this.tag.find("#button-settings").on("click", (function(_this) {
+        return function() {
+          wrapper.ws.cmd("fileGet", "content.json", function(res) {
+            var data, json_raw;
+            data = JSON.parse(res);
+            data["title"] = $("#settings-title").val();
+            data["description"] = $("#settings-description").val();
+            json_raw = unescape(encodeURIComponent(JSON.stringify(data, void 0, '\t')));
+            return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw)], function(res) {
+              if (res !== "ok") {
+                return wrapper.notifications.add("file-write", "error", "File write error: " + res);
+              } else {
+                wrapper.notifications.add("file-write", "done", "Site settings saved!", 5000);
+                return _this.updateHtmlTag();
+              }
+            });
+          });
+          return false;
+        };
+      })(this));
+      this.tag.find("#button-sign").on("click", (function(_this) {
+        return function() {
+          var inner_path;
+          inner_path = _this.tag.find("#select-contents").val();
+          if (wrapper.site_info.privatekey) {
+            wrapper.ws.cmd("siteSign", ["stored", inner_path], function(res) {
+              return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000);
+            });
+          } else {
+            wrapper.displayPrompt("Enter your private key:", "password", "Sign", function(privatekey) {
+              return wrapper.ws.cmd("siteSign", [privatekey, inner_path], function(res) {
+                if (res === "ok") {
+                  return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000);
+                }
+              });
+            });
+          }
+          return false;
+        };
+      })(this));
+      this.tag.find("#button-publish").on("click", (function(_this) {
+        return function() {
+          var inner_path;
+          inner_path = _this.tag.find("#select-contents").val();
+          _this.tag.find("#button-publish").addClass("loading");
+          return wrapper.ws.cmd("sitePublish", {
+            "inner_path": inner_path,
+            "sign": false
+          }, function() {
+            return _this.tag.find("#button-publish").removeClass("loading");
+          });
+        };
+      })(this));
+      return this.loadGlobe();
+    };
+
+    Sidebar.prototype.onClosed = function() {
+      $(window).off("resize");
+      $(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on(transitionEnd, (function(_this) {
+        return function(e) {
+          if (e.target === document.body) {
+            $(document.body).css("height", "auto").css("perspective", "").css("transition", "").off(transitionEnd);
+            return _this.unloadGlobe();
+          }
+        };
+      })(this));
+      return wrapper.setSiteInfo = this.original_set_site_info;
+    };
+
+    Sidebar.prototype.loadGlobe = function() {
+      if (this.tag.find(".globe").hasClass("loading")) {
+        return setTimeout(((function(_this) {
+          return function() {
+            if (typeof DAT === "undefined") {
+              return $.getScript("/uimedia/globe/all.js", _this.displayGlobe);
+            } else {
+              return _this.displayGlobe();
+            }
+          };
+        })(this)), 600);
+      }
+    };
+
+    Sidebar.prototype.displayGlobe = function() {
+      return wrapper.ws.cmd("sidebarGetPeers", [], (function(_this) {
+        return function(globe_data) {
+          if (_this.globe) {
+            _this.globe.scene.remove(_this.globe.points);
+            _this.globe.addData(globe_data, {
+              format: 'magnitude',
+              name: "hello",
+              animated: false
+            });
+            _this.globe.createPoints();
+          } else {
+            _this.globe = new DAT.Globe(_this.tag.find(".globe")[0], {
+              "imgDir": "/uimedia/globe/"
+            });
+            _this.globe.addData(globe_data, {
+              format: 'magnitude',
+              name: "hello"
+            });
+            _this.globe.createPoints();
+            _this.globe.animate();
+          }
+          return _this.tag.find(".globe").removeClass("loading");
+        };
+      })(this));
+    };
+
+    Sidebar.prototype.unloadGlobe = function() {
+      if (!this.globe) {
+        return false;
+      }
+      this.globe.unload();
+      return this.globe = null;
+    };
+
+    return Sidebar;
+
+  })(Class);
+
+  window.sidebar = new Sidebar();
+
+  window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend';
+
+}).call(this);
+
+
+
+/* ---- plugins/Sidebar/media/morphdom.js ---- */
+
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var specialElHandlers = {
+    /**
+     * Needed for IE. Apparently IE doesn't think
+     * that "selected" is an attribute when reading
+     * over the attributes using selectEl.attributes
+     */
+    OPTION: function(fromEl, toEl) {
+        if ((fromEl.selected = toEl.selected)) {
+            fromEl.setAttribute('selected', '');
+        } else {
+            fromEl.removeAttribute('selected', '');
+        }
+    },
+    /**
+     * The "value" attribute is special for the <input> element
+     * since it sets the initial value. Changing the "value"
+     * attribute without changing the "value" property will have
+     * no effect since it is only used to the set the initial value.
+     * Similar for the "checked" attribute.
+     */
+    /*INPUT: function(fromEl, toEl) {
+        fromEl.checked = toEl.checked;
+        fromEl.value = toEl.value;
+
+        if (!toEl.hasAttribute('checked')) {
+            fromEl.removeAttribute('checked');
+        }
+
+        if (!toEl.hasAttribute('value')) {
+            fromEl.removeAttribute('value');
+        }
+    }*/
+};
+
+function noop() {}
+
+/**
+ * Loop over all of the attributes on the target node and make sure the
+ * original DOM node has the same attributes. If an attribute
+ * found on the original node is not on the new node then remove it from
+ * the original node
+ * @param  {HTMLElement} fromNode
+ * @param  {HTMLElement} toNode
+ */
+function morphAttrs(fromNode, toNode) {
+    var attrs = toNode.attributes;
+    var i;
+    var attr;
+    var attrName;
+    var attrValue;
+    var foundAttrs = {};
+
+    for (i=attrs.length-1; i>=0; i--) {
+        attr = attrs[i];
+        if (attr.specified !== false) {
+            attrName = attr.name;
+            attrValue = attr.value;
+            foundAttrs[attrName] = true;
+
+            if (fromNode.getAttribute(attrName) !== attrValue) {
+                fromNode.setAttribute(attrName, attrValue);
+            }
+        }
+    }
+
+    // Delete any extra attributes found on the original DOM element that weren't
+    // found on the target element.
+    attrs = fromNode.attributes;
+
+    for (i=attrs.length-1; i>=0; i--) {
+        attr = attrs[i];
+        if (attr.specified !== false) {
+            attrName = attr.name;
+            if (!foundAttrs.hasOwnProperty(attrName)) {
+                fromNode.removeAttribute(attrName);
+            }
+        }
+    }
+}
+
+/**
+ * Copies the children of one DOM element to another DOM element
+ */
+function moveChildren(from, to) {
+    var curChild = from.firstChild;
+    while(curChild) {
+        var nextChild = curChild.nextSibling;
+        to.appendChild(curChild);
+        curChild = nextChild;
+    }
+    return to;
+}
+
+function morphdom(fromNode, toNode, options) {
+    if (!options) {
+        options = {};
+    }
+
+    if (typeof toNode === 'string') {
+        var newBodyEl = document.createElement('body');
+        newBodyEl.innerHTML = toNode;
+        toNode = newBodyEl.childNodes[0];
+    }
+
+    var savedEls = {}; // Used to save off DOM elements with IDs
+    var unmatchedEls = {};
+    var onNodeDiscarded = options.onNodeDiscarded || noop;
+    var onBeforeMorphEl = options.onBeforeMorphEl || noop;
+    var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
+
+    function removeNodeHelper(node, nestedInSavedEl) {
+        var id = node.id;
+        // If the node has an ID then save it off since we will want
+        // to reuse it in case the target DOM tree has a DOM element
+        // with the same ID
+        if (id) {
+            savedEls[id] = node;
+        } else if (!nestedInSavedEl) {
+            // If we are not nested in a saved element then we know that this node has been
+            // completely discarded and will not exist in the final DOM.
+            onNodeDiscarded(node);
+        }
+
+        if (node.nodeType === 1) {
+            var curChild = node.firstChild;
+            while(curChild) {
+                removeNodeHelper(curChild, nestedInSavedEl || id);
+                curChild = curChild.nextSibling;
+            }
+        }
+    }
+
+    function walkDiscardedChildNodes(node) {
+        if (node.nodeType === 1) {
+            var curChild = node.firstChild;
+            while(curChild) {
+
+
+                if (!curChild.id) {
+                    // We only want to handle nodes that don't have an ID to avoid double
+                    // walking the same saved element.
+
+                    onNodeDiscarded(curChild);
+
+                    // Walk recursively
+                    walkDiscardedChildNodes(curChild);
+                }
+
+                curChild = curChild.nextSibling;
+            }
+        }
+    }
+
+    function removeNode(node, parentNode, alreadyVisited) {
+        parentNode.removeChild(node);
+
+        if (alreadyVisited) {
+            if (!node.id) {
+                onNodeDiscarded(node);
+                walkDiscardedChildNodes(node);
+            }
+        } else {
+            removeNodeHelper(node);
+        }
+    }
+
+    function morphEl(fromNode, toNode, alreadyVisited) {
+        if (toNode.id) {
+            // If an element with an ID is being morphed then it is will be in the final
+            // DOM so clear it out of the saved elements collection
+            delete savedEls[toNode.id];
+        }
+
+        if (onBeforeMorphEl(fromNode, toNode) === false) {
+            return;
+        }
+
+        morphAttrs(fromNode, toNode);
+
+        if (onBeforeMorphElChildren(fromNode, toNode) === false) {
+            return;
+        }
+
+        var curToNodeChild = toNode.firstChild;
+        var curFromNodeChild = fromNode.firstChild;
+        var curToNodeId;
+
+        var fromNextSibling;
+        var toNextSibling;
+        var savedEl;
+        var unmatchedEl;
+
+        outer: while(curToNodeChild) {
+            toNextSibling = curToNodeChild.nextSibling;
+            curToNodeId = curToNodeChild.id;
+
+            while(curFromNodeChild) {
+                var curFromNodeId = curFromNodeChild.id;
+                fromNextSibling = curFromNodeChild.nextSibling;
+
+                if (!alreadyVisited) {
+                    if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
+                        unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
+                        morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
+                        curFromNodeChild = fromNextSibling;
+                        continue;
+                    }
+                }
+
+                var curFromNodeType = curFromNodeChild.nodeType;
+
+                if (curFromNodeType === curToNodeChild.nodeType) {
+                    var isCompatible = false;
+
+                    if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
+                        if (curFromNodeChild.tagName === curToNodeChild.tagName) {
+                            // We have compatible DOM elements
+                            if (curFromNodeId || curToNodeId) {
+                                // If either DOM element has an ID then we handle
+                                // those differently since we want to match up
+                                // by ID
+                                if (curToNodeId === curFromNodeId) {
+                                    isCompatible = true;
+                                }
+                            } else {
+                                isCompatible = true;
+                            }
+                        }
+
+                        if (isCompatible) {
+                            // We found compatible DOM elements so add a
+                            // task to morph the compatible DOM elements
+                            morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
+                        }
+                    } else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
+                        isCompatible = true;
+                        curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
+                    }
+
+                    if (isCompatible) {
+                        curToNodeChild = toNextSibling;
+                        curFromNodeChild = fromNextSibling;
+                        continue outer;
+                    }
+                }
+
+                // No compatible match so remove the old node from the DOM
+                removeNode(curFromNodeChild, fromNode, alreadyVisited);
+
+                curFromNodeChild = fromNextSibling;
+            }
+
+            if (curToNodeId) {
+                if ((savedEl = savedEls[curToNodeId])) {
+                    morphEl(savedEl, curToNodeChild, true);
+                    curToNodeChild = savedEl; // We want to append the saved element instead
+                } else {
+                    // The current DOM element in the target tree has an ID
+                    // but we did not find a match in any of the corresponding
+                    // siblings. We just put the target element in the old DOM tree
+                    // but if we later find an element in the old DOM tree that has
+                    // a matching ID then we will replace the target element
+                    // with the corresponding old element and morph the old element
+                    unmatchedEls[curToNodeId] = curToNodeChild;
+                }
+            }
+
+            // If we got this far then we did not find a candidate match for our "to node"
+            // and we exhausted all of the children "from" nodes. Therefore, we will just
+            // append the current "to node" to the end
+            fromNode.appendChild(curToNodeChild);
+
+            curToNodeChild = toNextSibling;
+            curFromNodeChild = fromNextSibling;
+        }
+
+        // We have processed all of the "to nodes". If curFromNodeChild is non-null then
+        // we still have some from nodes left over that need to be removed
+        while(curFromNodeChild) {
+            fromNextSibling = curFromNodeChild.nextSibling;
+            removeNode(curFromNodeChild, fromNode, alreadyVisited);
+            curFromNodeChild = fromNextSibling;
+        }
+
+        var specialElHandler = specialElHandlers[fromNode.tagName];
+        if (specialElHandler) {
+            specialElHandler(fromNode, toNode);
+        }
+    }
+
+    var morphedNode = fromNode;
+    var morphedNodeType = morphedNode.nodeType;
+    var toNodeType = toNode.nodeType;
+
+    // Handle the case where we are given two DOM nodes that are not
+    // compatible (e.g. <div> --> <span> or <div> --> TEXT)
+    if (morphedNodeType === 1) {
+        if (toNodeType === 1) {
+            if (morphedNode.tagName !== toNode.tagName) {
+                onNodeDiscarded(fromNode);
+                morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
+            }
+        } else {
+            // Going from an element node to a text node
+            return toNode;
+        }
+    } else if (morphedNodeType === 3) { // Text node
+        if (toNodeType === 3) {
+            morphedNode.nodeValue = toNode.nodeValue;
+            return morphedNode;
+        } else {
+            onNodeDiscarded(fromNode);
+            // Text node to something else
+            return toNode;
+        }
+    }
+
+    morphEl(morphedNode, toNode, false);
+
+    // Fire the "onNodeDiscarded" event for any saved elements
+    // that never found a new home in the morphed DOM
+    for (var savedElId in savedEls) {
+        if (savedEls.hasOwnProperty(savedElId)) {
+            var savedEl = savedEls[savedElId];
+            onNodeDiscarded(savedEl);
+            walkDiscardedChildNodes(savedEl);
+        }
+    }
+
+    if (morphedNode !== fromNode && fromNode.parentNode) {
+        fromNode.parentNode.replaceChild(morphedNode, fromNode);
+    }
+
+    return morphedNode;
+}
+
+module.exports = morphdom;
+},{}]},{},[1])(1)
+});

+ 340 - 0
plugins/Sidebar/media/morphdom.js

@@ -0,0 +1,340 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var specialElHandlers = {
+    /**
+     * Needed for IE. Apparently IE doesn't think
+     * that "selected" is an attribute when reading
+     * over the attributes using selectEl.attributes
+     */
+    OPTION: function(fromEl, toEl) {
+        if ((fromEl.selected = toEl.selected)) {
+            fromEl.setAttribute('selected', '');
+        } else {
+            fromEl.removeAttribute('selected', '');
+        }
+    },
+    /**
+     * The "value" attribute is special for the <input> element
+     * since it sets the initial value. Changing the "value"
+     * attribute without changing the "value" property will have
+     * no effect since it is only used to the set the initial value.
+     * Similar for the "checked" attribute.
+     */
+    /*INPUT: function(fromEl, toEl) {
+        fromEl.checked = toEl.checked;
+        fromEl.value = toEl.value;
+
+        if (!toEl.hasAttribute('checked')) {
+            fromEl.removeAttribute('checked');
+        }
+
+        if (!toEl.hasAttribute('value')) {
+            fromEl.removeAttribute('value');
+        }
+    }*/
+};
+
+function noop() {}
+
+/**
+ * Loop over all of the attributes on the target node and make sure the
+ * original DOM node has the same attributes. If an attribute
+ * found on the original node is not on the new node then remove it from
+ * the original node
+ * @param  {HTMLElement} fromNode
+ * @param  {HTMLElement} toNode
+ */
+function morphAttrs(fromNode, toNode) {
+    var attrs = toNode.attributes;
+    var i;
+    var attr;
+    var attrName;
+    var attrValue;
+    var foundAttrs = {};
+
+    for (i=attrs.length-1; i>=0; i--) {
+        attr = attrs[i];
+        if (attr.specified !== false) {
+            attrName = attr.name;
+            attrValue = attr.value;
+            foundAttrs[attrName] = true;
+
+            if (fromNode.getAttribute(attrName) !== attrValue) {
+                fromNode.setAttribute(attrName, attrValue);
+            }
+        }
+    }
+
+    // Delete any extra attributes found on the original DOM element that weren't
+    // found on the target element.
+    attrs = fromNode.attributes;
+
+    for (i=attrs.length-1; i>=0; i--) {
+        attr = attrs[i];
+        if (attr.specified !== false) {
+            attrName = attr.name;
+            if (!foundAttrs.hasOwnProperty(attrName)) {
+                fromNode.removeAttribute(attrName);
+            }
+        }
+    }
+}
+
+/**
+ * Copies the children of one DOM element to another DOM element
+ */
+function moveChildren(from, to) {
+    var curChild = from.firstChild;
+    while(curChild) {
+        var nextChild = curChild.nextSibling;
+        to.appendChild(curChild);
+        curChild = nextChild;
+    }
+    return to;
+}
+
+function morphdom(fromNode, toNode, options) {
+    if (!options) {
+        options = {};
+    }
+
+    if (typeof toNode === 'string') {
+        var newBodyEl = document.createElement('body');
+        newBodyEl.innerHTML = toNode;
+        toNode = newBodyEl.childNodes[0];
+    }
+
+    var savedEls = {}; // Used to save off DOM elements with IDs
+    var unmatchedEls = {};
+    var onNodeDiscarded = options.onNodeDiscarded || noop;
+    var onBeforeMorphEl = options.onBeforeMorphEl || noop;
+    var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
+
+    function removeNodeHelper(node, nestedInSavedEl) {
+        var id = node.id;
+        // If the node has an ID then save it off since we will want
+        // to reuse it in case the target DOM tree has a DOM element
+        // with the same ID
+        if (id) {
+            savedEls[id] = node;
+        } else if (!nestedInSavedEl) {
+            // If we are not nested in a saved element then we know that this node has been
+            // completely discarded and will not exist in the final DOM.
+            onNodeDiscarded(node);
+        }
+
+        if (node.nodeType === 1) {
+            var curChild = node.firstChild;
+            while(curChild) {
+                removeNodeHelper(curChild, nestedInSavedEl || id);
+                curChild = curChild.nextSibling;
+            }
+        }
+    }
+
+    function walkDiscardedChildNodes(node) {
+        if (node.nodeType === 1) {
+            var curChild = node.firstChild;
+            while(curChild) {
+
+
+                if (!curChild.id) {
+                    // We only want to handle nodes that don't have an ID to avoid double
+                    // walking the same saved element.
+
+                    onNodeDiscarded(curChild);
+
+                    // Walk recursively
+                    walkDiscardedChildNodes(curChild);
+                }
+
+                curChild = curChild.nextSibling;
+            }
+        }
+    }
+
+    function removeNode(node, parentNode, alreadyVisited) {
+        parentNode.removeChild(node);
+
+        if (alreadyVisited) {
+            if (!node.id) {
+                onNodeDiscarded(node);
+                walkDiscardedChildNodes(node);
+            }
+        } else {
+            removeNodeHelper(node);
+        }
+    }
+
+    function morphEl(fromNode, toNode, alreadyVisited) {
+        if (toNode.id) {
+            // If an element with an ID is being morphed then it is will be in the final
+            // DOM so clear it out of the saved elements collection
+            delete savedEls[toNode.id];
+        }
+
+        if (onBeforeMorphEl(fromNode, toNode) === false) {
+            return;
+        }
+
+        morphAttrs(fromNode, toNode);
+
+        if (onBeforeMorphElChildren(fromNode, toNode) === false) {
+            return;
+        }
+
+        var curToNodeChild = toNode.firstChild;
+        var curFromNodeChild = fromNode.firstChild;
+        var curToNodeId;
+
+        var fromNextSibling;
+        var toNextSibling;
+        var savedEl;
+        var unmatchedEl;
+
+        outer: while(curToNodeChild) {
+            toNextSibling = curToNodeChild.nextSibling;
+            curToNodeId = curToNodeChild.id;
+
+            while(curFromNodeChild) {
+                var curFromNodeId = curFromNodeChild.id;
+                fromNextSibling = curFromNodeChild.nextSibling;
+
+                if (!alreadyVisited) {
+                    if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
+                        unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
+                        morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
+                        curFromNodeChild = fromNextSibling;
+                        continue;
+                    }
+                }
+
+                var curFromNodeType = curFromNodeChild.nodeType;
+
+                if (curFromNodeType === curToNodeChild.nodeType) {
+                    var isCompatible = false;
+
+                    if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
+                        if (curFromNodeChild.tagName === curToNodeChild.tagName) {
+                            // We have compatible DOM elements
+                            if (curFromNodeId || curToNodeId) {
+                                // If either DOM element has an ID then we handle
+                                // those differently since we want to match up
+                                // by ID
+                                if (curToNodeId === curFromNodeId) {
+                                    isCompatible = true;
+                                }
+                            } else {
+                                isCompatible = true;
+                            }
+                        }
+
+                        if (isCompatible) {
+                            // We found compatible DOM elements so add a
+                            // task to morph the compatible DOM elements
+                            morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
+                        }
+                    } else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
+                        isCompatible = true;
+                        curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
+                    }
+
+                    if (isCompatible) {
+                        curToNodeChild = toNextSibling;
+                        curFromNodeChild = fromNextSibling;
+                        continue outer;
+                    }
+                }
+
+                // No compatible match so remove the old node from the DOM
+                removeNode(curFromNodeChild, fromNode, alreadyVisited);
+
+                curFromNodeChild = fromNextSibling;
+            }
+
+            if (curToNodeId) {
+                if ((savedEl = savedEls[curToNodeId])) {
+                    morphEl(savedEl, curToNodeChild, true);
+                    curToNodeChild = savedEl; // We want to append the saved element instead
+                } else {
+                    // The current DOM element in the target tree has an ID
+                    // but we did not find a match in any of the corresponding
+                    // siblings. We just put the target element in the old DOM tree
+                    // but if we later find an element in the old DOM tree that has
+                    // a matching ID then we will replace the target element
+                    // with the corresponding old element and morph the old element
+                    unmatchedEls[curToNodeId] = curToNodeChild;
+                }
+            }
+
+            // If we got this far then we did not find a candidate match for our "to node"
+            // and we exhausted all of the children "from" nodes. Therefore, we will just
+            // append the current "to node" to the end
+            fromNode.appendChild(curToNodeChild);
+
+            curToNodeChild = toNextSibling;
+            curFromNodeChild = fromNextSibling;
+        }
+
+        // We have processed all of the "to nodes". If curFromNodeChild is non-null then
+        // we still have some from nodes left over that need to be removed
+        while(curFromNodeChild) {
+            fromNextSibling = curFromNodeChild.nextSibling;
+            removeNode(curFromNodeChild, fromNode, alreadyVisited);
+            curFromNodeChild = fromNextSibling;
+        }
+
+        var specialElHandler = specialElHandlers[fromNode.tagName];
+        if (specialElHandler) {
+            specialElHandler(fromNode, toNode);
+        }
+    }
+
+    var morphedNode = fromNode;
+    var morphedNodeType = morphedNode.nodeType;
+    var toNodeType = toNode.nodeType;
+
+    // Handle the case where we are given two DOM nodes that are not
+    // compatible (e.g. <div> --> <span> or <div> --> TEXT)
+    if (morphedNodeType === 1) {
+        if (toNodeType === 1) {
+            if (morphedNode.tagName !== toNode.tagName) {
+                onNodeDiscarded(fromNode);
+                morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
+            }
+        } else {
+            // Going from an element node to a text node
+            return toNode;
+        }
+    } else if (morphedNodeType === 3) { // Text node
+        if (toNodeType === 3) {
+            morphedNode.nodeValue = toNode.nodeValue;
+            return morphedNode;
+        } else {
+            onNodeDiscarded(fromNode);
+            // Text node to something else
+            return toNode;
+        }
+    }
+
+    morphEl(morphedNode, toNode, false);
+
+    // Fire the "onNodeDiscarded" event for any saved elements
+    // that never found a new home in the morphed DOM
+    for (var savedElId in savedEls) {
+        if (savedEls.hasOwnProperty(savedElId)) {
+            var savedEl = savedEls[savedElId];
+            onNodeDiscarded(savedEl);
+            walkDiscardedChildNodes(savedEl);
+        }
+    }
+
+    if (morphedNode !== fromNode && fromNode.parentNode) {
+        fromNode.parentNode.replaceChild(morphedNode, fromNode);
+    }
+
+    return morphedNode;
+}
+
+module.exports = morphdom;
+},{}]},{},[1])(1)
+});

+ 3 - 1
plugins/Stats/StatsPlugin.py

@@ -116,7 +116,7 @@ class UiRequestPlugin(object):
         # Sites
         yield "<br><br><b>Sites</b>:"
         yield "<table>"
-        yield "<tr><th>address</th> <th>connected</th> <th title='connected/good/total'>peers</th> <th>content.json</th> </tr>"
+        yield "<tr><th>address</th> <th>connected</th> <th title='connected/good/total'>peers</th> <th>content.json</th> <th>out</th> <th>in</th>  </tr>"
         for site in self.server.sites.values():
             yield self.formatTableRow([
                 (
@@ -130,6 +130,8 @@ class UiRequestPlugin(object):
                     len(site.peers)
                 )),
                 ("%s", len(site.content_manager.contents)),
+                ("%.0fkB", site.settings.get("bytes_sent", 0) / 1024),
+                ("%.0fkB", site.settings.get("bytes_recv", 0) / 1024),
             ])
             yield "<tr><td id='peers_%s' style='display: none; white-space: pre'>" % site.address
             for key, peer in site.peers.items():

+ 3 - 0
plugins/Zeroname/UiRequestPlugin.py

@@ -25,6 +25,9 @@ class UiRequestPlugin(object):
         referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "")  # Remove site address
         referer_path = re.sub("\?.*", "", referer_path)  # Remove http params
 
+        if not re.sub("^http[s]{0,1}://", "", referer).startswith(self.env["HTTP_HOST"]):  # Different origin
+            return False
+
         if self.isProxyRequest():  # Match to site domain
             referer = re.sub("^http://zero[/]+", "http://", referer)  # Allow /zero access
             referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1)

+ 2 - 2
src/Config.py

@@ -7,8 +7,8 @@ import ConfigParser
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.3.1"
-        self.rev = 338
+        self.version = "0.3.2"
+        self.rev = 351
         self.argv = argv
         self.action = None
         self.createParser()

+ 3 - 0
src/Connection/Connection.py

@@ -176,6 +176,9 @@ class Connection(object):
         self.last_message_time = time.time()
         if message.get("cmd") == "response":  # New style response
             if message["to"] in self.waiting_requests:
+                if self.last_send_time:
+                    ping = time.time() - self.last_send_time
+                    self.last_ping_delay = ping
                 self.waiting_requests[message["to"]].set(message)  # Set the response to event
                 del self.waiting_requests[message["to"]]
             elif message["to"] == 0:  # Other peers handshake

+ 2 - 2
src/Db/Db.py

@@ -12,14 +12,14 @@ opened_dbs = []
 
 
 # Close idle databases to save some memory
-def cleanup():
+def dbCleanup():
     while 1:
         time.sleep(60 * 5)
         for db in opened_dbs[:]:
             if time.time() - db.last_query_time > 60 * 3:
                 db.close()
 
-gevent.spawn(cleanup)
+gevent.spawn(dbCleanup)
 
 
 class Db:

+ 14 - 7
src/File/FileRequest.py

@@ -139,10 +139,11 @@ class FileRequest(object):
             with StreamingMsgpack.FilePart(file_path, "rb") as file:
                 file.seek(params["location"])
                 file.read_bytes = FILE_BUFF
+                file_size = os.fstat(file.fileno()).st_size
                 back = {
                     "body": file,
-                    "size": os.fstat(file.fileno()).st_size,
-                    "location": min(file.tell() + FILE_BUFF, os.fstat(file.fileno()).st_size)
+                    "size": file_size,
+                    "location": min(file.tell() + FILE_BUFF, file_size)
                 }
                 if config.debug_socket:
                     self.log.debug(
@@ -150,8 +151,11 @@ class FileRequest(object):
                         (file_path, params["location"], back["location"])
                     )
                 self.response(back, streaming=True)
+
+                bytes_sent = min(FILE_BUFF, file_size - params["location"])  # Number of bytes we going to send
+                site.settings["bytes_sent"] = site.settings.get("bytes_sent", 0) + bytes_sent
             if config.debug_socket:
-                self.log.debug("File %s sent" % file_path)
+                self.log.debug("File %s at position %s sent %s bytes" % (file_path, params["location"], bytes_sent))
 
             # Add peer to site if not added before
             connected_peer = site.addPeer(self.connection.ip, self.connection.port)
@@ -174,10 +178,11 @@ class FileRequest(object):
                 self.log.debug("Opening file: %s" % params["inner_path"])
             with site.storage.open(params["inner_path"]) as file:
                 file.seek(params["location"])
-                stream_bytes = min(FILE_BUFF, os.fstat(file.fileno()).st_size-params["location"])
+                file_size = os.fstat(file.fileno()).st_size
+                stream_bytes = min(FILE_BUFF, file_size - params["location"])
                 back = {
-                    "size": os.fstat(file.fileno()).st_size,
-                    "location": min(file.tell() + FILE_BUFF, os.fstat(file.fileno()).st_size),
+                    "size": file_size,
+                    "location": min(file.tell() + FILE_BUFF, file_size),
                     "stream_bytes": stream_bytes
                 }
                 if config.debug_socket:
@@ -187,8 +192,10 @@ class FileRequest(object):
                     )
                 self.response(back)
                 self.sendRawfile(file, read_bytes=FILE_BUFF)
+
+                site.settings["bytes_sent"] = site.settings.get("bytes_sent", 0) + stream_bytes
             if config.debug_socket:
-                self.log.debug("File %s sent" % params["inner_path"])
+                self.log.debug("File %s at position %s sent %s bytes" % (params["inner_path"], params["location"], stream_bytes))
 
             # Add peer to site if not added before
             connected_peer = site.addPeer(self.connection.ip, self.connection.port)

+ 2 - 0
src/Peer/Peer.py

@@ -151,6 +151,7 @@ class Peer(object):
 
         self.download_bytes += back["location"]
         self.download_time += (time.time() - s)
+        self.site.settings["bytes_recv"] = self.site.settings.get("bytes_recv", 0) + back["location"]
         buff.seek(0)
         return buff
 
@@ -177,6 +178,7 @@ class Peer(object):
 
         self.download_bytes += back["location"]
         self.download_time += (time.time() - s)
+        self.site.settings["bytes_recv"] = self.site.settings.get("bytes_recv", 0) + back["location"]
         buff.seek(0)
         return buff
 

+ 8 - 5
src/Site/Site.py

@@ -137,11 +137,11 @@ class Site:
 
         self.log.debug("%s: Downloading %s includes..." % (inner_path, len(include_threads)))
         gevent.joinall(include_threads)
-        self.log.debug("%s: Includes downloaded" % inner_path)
+        self.log.debug("%s: Includes download ended" % inner_path)
 
         self.log.debug("%s: Downloading %s files, changed: %s..." % (inner_path, len(file_threads), len(changed)))
         gevent.joinall(file_threads)
-        self.log.debug("%s: All file downloaded in %.2fs" % (inner_path, time.time() - s))
+        self.log.debug("%s: DownloadContent ended in %.2fs" % (inner_path, time.time() - s))
 
         return True
 
@@ -159,7 +159,10 @@ class Site:
     # Download all files of the site
     @util.Noparallel(blocking=False)
     def download(self, check_size=False, blind_includes=False):
-        self.log.debug("Start downloading, bad_files: %s, check_size: %s, blind_includes: %s" % (self.bad_files, check_size, blind_includes))
+        self.log.debug(
+            "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s" %
+            (self.bad_files, check_size, blind_includes)
+        )
         gevent.spawn(self.announce)
         if check_size:  # Check the size first
             valid = self.downloadContent(download_files=False)  # Just download content.json files
@@ -221,7 +224,7 @@ class Site:
         for i in range(3):
             updaters.append(gevent.spawn(self.updater, peers_try, queried, since))
 
-        gevent.joinall(updaters, timeout=5)  # Wait 5 sec to workers
+        gevent.joinall(updaters, timeout=10)  # Wait 10 sec to workers done query modifications
         time.sleep(0.1)
         self.log.debug("Queried listModifications from: %s" % queried)
         return queried
@@ -420,7 +423,7 @@ class Site:
         elif self.settings["serving"] is False:  # Site not serving
             return False
         else:  # Wait until file downloaded
-            self.bad_files[inner_path] = self.bad_files.get(inner_path,0)+1  # Mark as bad file
+            self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1  # Mark as bad file
             if not self.content_manager.contents.get("content.json"):  # No content.json, download it first!
                 self.log.debug("Need content.json first")
                 gevent.spawn(self.announce)

+ 5 - 0
src/Site/SiteStorage.py

@@ -210,6 +210,11 @@ class SiteStorage:
             raise Exception("File not allowed: %s" % file_path)
         return file_path
 
+    # Get site dir relative path
+    def getInnerPath(self, path):
+        inner_path = re.sub("^%s/" % re.escape(self.directory), "", path)
+        return inner_path
+
     # Verify all files sha512sum using content.json
     def verifyFiles(self, quick_check=False):  # Fast = using file size
         bad_files = []

+ 15 - 9
src/Ui/UiWebsocket.py

@@ -57,14 +57,18 @@ class UiWebsocket(object):
         while True:
             try:
                 message = ws.receive()
-                if message:
-                    self.handleRequest(message)
             except Exception, err:
-                if err.message != 'Connection is already closed':
+                self.log.error("WebSocket receive error: %s" % err)
+                return "Bye."  # Close connection
+
+            if message:
+                try:
+                    self.handleRequest(message)
+                except Exception, err:
                     if config.debug:  # Allow websocket errors to appear on /Debug
                         sys.modules["main"].DebugHook.handleError()
-                    self.log.error("WebSocket error: %s" % Debug.formatException(err))
-                return "Bye."
+                    self.log.error("WebSocket handleRequest error: %s" % err)
+                    self.cmd("error", "Internal error: %s" % err)
 
     # Event in a channel
     def event(self, channel, *params):
@@ -138,8 +142,10 @@ class UiWebsocket(object):
             func(req["id"], **params)
         elif type(params) is list:
             func(req["id"], *params)
-        else:
+        elif params:
             func(req["id"], params)
+        else:
+            func(req["id"])
 
     # Format site info
     def formatSiteInfo(self, site, create_user=True):
@@ -170,7 +176,7 @@ class UiWebsocket(object):
             "bad_files": len(site.bad_files),
             "size_limit": site.getSizeLimit(),
             "next_size_limit": site.getNextSizeLimit(),
-            "peers": site.settings.get("peers", len(site.peers)),
+            "peers": max(site.settings.get("peers", 0), len(site.peers)),
             "started_task_num": site.worker_manager.started_task_num,
             "tasks": len(site.worker_manager.tasks),
             "workers": len(site.worker_manager.workers),
@@ -404,7 +410,7 @@ class UiWebsocket(object):
             if auth_address == cert["auth_address"]:
                 active = domain
             title = cert["auth_user_name"] + "@" + domain
-            if domain in accepted_domains:
+            if domain in accepted_domains or not accepted_domains:
                 accounts.append([domain, title, ""])
             else:
                 accounts.append([domain, title, "disabled"])
@@ -527,7 +533,7 @@ class UiWebsocket(object):
         gevent.spawn(new_site.announce)
 
     def actionSiteSetLimit(self, to, size_limit):
-        self.site.settings["size_limit"] = size_limit
+        self.site.settings["size_limit"] = int(size_limit)
         self.site.saveSettings()
         self.response(to, "Site size limit changed to %sMB" % size_limit)
         self.site.download(blind_includes=True)

+ 7 - 1
src/Ui/media/Wrapper.coffee

@@ -49,7 +49,11 @@ class Wrapper
 			else
 				@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])
+			type = message.params[0]
+			id = "notification-#{message.id}"
+			if "-" in message.params[0]  # - in first param: message id definied
+				[id, type] = message.params[0].split("-")
+			@notifications.add(id, type, 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
@@ -57,6 +61,8 @@ class Wrapper
 			@sendInner message # Pass to inner frame
 			if message.params.address == @address # Current page
 				@setSiteInfo message.params
+		else if cmd == "error"
+			@notifications.add("notification-#{message.id}", "error", message.params, 0)
 		else if cmd == "updating" # Close connection
 			@ws.ws.close()
 			@ws.onCloseWebsocket(null, 4000)

+ 5 - 1
src/Ui/media/Wrapper.css

@@ -7,13 +7,17 @@ a { color: black }
 #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0px } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/
 #inner-iframe.back { transform: scale(0.95) translate(-300px, 0px); opacity: 0.4 }
 
-.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; border-radius: 2px; text-decoration: none; transition: all 0.5s; }
+.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; border-radius: 2px; text-decoration: none; transition: all 0.5s; background-position: left center; }
 .button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; transition: none }
 .button:active { position: relative; top: 1px }
 
 .button-Delete { background-color: #e74c3c; border-bottom-color: #c0392b; color: white }
 .button-Delete:hover { background-color: #FF5442; border-bottom-color: #8E2B21 }
 
+.button.loading {
+	color: rgba(0,0,0,0); background: #999 url(img/loading.gif) no-repeat center center;
+	transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666
+}
 
 /* Fixbutton */
 

+ 5 - 1
src/Ui/media/all.css

@@ -12,13 +12,17 @@ a { color: black }
 #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0px } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/
 #inner-iframe.back { -webkit-transform: scale(0.95) translate(-300px, 0px); -moz-transform: scale(0.95) translate(-300px, 0px); -o-transform: scale(0.95) translate(-300px, 0px); -ms-transform: scale(0.95) translate(-300px, 0px); transform: scale(0.95) translate(-300px, 0px) ; opacity: 0.4 }
 
-.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; }
+.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; background-position: left center; }
 .button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none  }
 .button:active { position: relative; top: 1px }
 
 .button-Delete { background-color: #e74c3c; border-bottom-color: #c0392b; color: white }
 .button-Delete:hover { background-color: #FF5442; border-bottom-color: #8E2B21 }
 
+.button.loading {
+	color: rgba(0,0,0,0); background: #999 url(img/loading.gif) no-repeat center center;
+	-webkit-transition: all 0.5s ease-out ; -moz-transition: all 0.5s ease-out ; -o-transition: all 0.5s ease-out ; -ms-transition: all 0.5s ease-out ; transition: all 0.5s ease-out  ; pointer-events: none; border-bottom: 2px solid #666
+}
 
 /* Fixbutton */
 

+ 10 - 2
src/Ui/media/all.js

@@ -758,6 +758,7 @@ jQuery.extend( jQuery.easing,
 (function() {
   var Wrapper, origin, proto, ws_url,
     __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
     __slice = [].slice;
 
   Wrapper = (function() {
@@ -810,7 +811,7 @@ jQuery.extend( jQuery.easing,
     }
 
     Wrapper.prototype.onMessageWebsocket = function(e) {
-      var cmd, message;
+      var cmd, id, message, type, _ref;
       message = JSON.parse(e.data);
       cmd = message.cmd;
       if (cmd === "response") {
@@ -820,7 +821,12 @@ jQuery.extend( jQuery.easing,
           return this.sendInner(message);
         }
       } else if (cmd === "notification") {
-        return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]);
+        type = message.params[0];
+        id = "notification-" + message.id;
+        if (__indexOf.call(message.params[0], "-") >= 0) {
+          _ref = message.params[0].split("-"), id = _ref[0], type = _ref[1];
+        }
+        return this.notifications.add(id, type, 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) {
@@ -832,6 +838,8 @@ jQuery.extend( jQuery.easing,
         if (message.params.address === this.address) {
           return this.setSiteInfo(message.params);
         }
+      } else if (cmd === "error") {
+        return this.notifications.add("notification-" + message.id, "error", message.params, 0);
       } else if (cmd === "updating") {
         this.ws.ws.close();
         return this.ws.onCloseWebsocket(null, 4000);

BIN
src/Ui/media/img/loading-circle.gif


BIN
src/Ui/media/img/loading.gif


+ 2 - 2
src/util/RateLimit.py

@@ -78,14 +78,14 @@ def call(event, allowed_again=10, func=None, *args, **kwargs):
 
 
 # Cleanup expired events every 3 minutes
-def cleanup():
+def rateLimitCleanup():
     while 1:
         expired = time.time() - 60 * 2  # Cleanup if older than 2 minutes
         for event in called_db.keys():
             if called_db[event] < expired:
                 del called_db[event]
         time.sleep(60 * 3)  # Every 3 minutes
-gevent.spawn(cleanup)
+gevent.spawn(rateLimitCleanup)
 
 
 if __name__ == "__main__":

Some files were not shown because too many files changed in this diff