diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py
new file mode 100644
index 00000000..a778431c
--- /dev/null
+++ b/plugins/MergerSite/MergerSitePlugin.py
@@ -0,0 +1,305 @@
+import re
+
+from Plugin import PluginManager
+from util import RateLimit
+from util import helper
+
+if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads
+ merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]}
+ merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...}
+ merged_to_merger = {} # {address: [site1, site2, ...]} cache
+ site_manager = None # Site manager for merger sites
+
+
+# Check if the site has permission to this merger site
+def checkMergerPath(address, inner_path):
+ merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
+ if merged_match:
+ merger_type = merged_match.group(1)
+ # Check if merged site is allowed to include other sites
+ if merger_type in merger_db.get(address, []):
+ # Check if included site allows to include
+ merged_address = merged_match.group(2)
+ if merged_db.get(merged_address) == merger_type:
+ inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
+ return merged_address, inner_path
+ else:
+ raise Exception("Merger site (%s) does not have permission for merged site: %s" % (merger_type, merged_address))
+ else:
+ raise Exception("No merger (%s) permission to load:
%s (%s not in %s)" % (
+ address, inner_path, merger_type, merger_db.get(address, []))
+ )
+ else:
+ raise Exception("Invalid merger path: %s" % inner_path)
+
+
+@PluginManager.registerTo("UiWebsocket")
+class UiWebsocketPlugin(object):
+ # Download new site
+ def actionMergerSiteAdd(self, to, addresses):
+ if type(addresses) != list:
+ # Single site add
+ addresses = [addresses]
+ # Check if the site has merger permission
+ merger_types = merger_db.get(self.site.address)
+ if not merger_types:
+ return self.response(to, {"error": "Not a merger site"})
+
+ if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1:
+ # Without confirmation if only one site address and not called in last 10 sec
+ self.cbMergerSiteAdd(to, addresses)
+ else:
+ self.cmd(
+ "confirm",
+ ["Add %s new site?" % len(addresses), "Add"],
+ lambda (res): self.cbMergerSiteAdd(to, addresses)
+ )
+ self.response(to, "ok")
+
+ # Callback of adding new site confirmation
+ def cbMergerSiteAdd(self, to, addresses):
+ added = 0
+ for address in addresses:
+ added += 1
+ site_manager.need(address)
+ if added:
+ self.cmd("notification", ["done", "Added %s new site" % added, 5000])
+ RateLimit.called(self.site.address + "-MergerSiteAdd")
+ site_manager.updateMergerSites()
+
+ # Delete a merged site
+ def actionMergerSiteDelete(self, to, address):
+ site = self.server.sites.get(address)
+ if not site:
+ return self.response(to, {"error": "No site found: %s" % address})
+
+ merger_types = merger_db.get(self.site.address)
+ if not merger_types:
+ return self.response(to, {"error": "Not a merger site"})
+ if merged_db.get(address) not in merger_types:
+ return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)})
+
+ self.cmd("notification", ["done", "Site deleted: %s" % address, 5000])
+ self.response(to, "ok")
+
+ # Lists merged sites
+ def actionMergerSiteList(self, to, query_site_info=False):
+ merger_types = merger_db.get(self.site.address)
+ ret = {}
+ if not merger_types:
+ return self.response(to, {"error": "Not a merger site"})
+ for address, merged_type in merged_db.iteritems():
+ if merged_type not in merger_types:
+ continue # Site not for us
+ if query_site_info:
+ site = self.server.sites.get(address)
+ ret[address] = self.formatSiteInfo(site, create_user=False)
+ else:
+ ret[address] = merged_type
+ self.response(to, ret)
+
+ # Add support merger sites for file commands
+ def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs):
+ func = getattr(super(UiWebsocketPlugin, self), func_name)
+ if inner_path.startswith("merged-"):
+ merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
+
+ # Set the same cert for merged site
+ merger_cert = self.user.getSiteData(self.site.address).get("cert")
+ if merger_cert:
+ self.user.setCert(merged_address, merger_cert)
+
+ site_before = self.site # Save to be able to change it back after we ran the command
+ self.site = self.server.sites.get(merged_address) # Change the site to the merged one
+ try:
+ back = func(to, merged_inner_path, *args, **kwargs)
+ finally:
+ self.site = site_before # Change back to original site
+ return back
+ else:
+ return func(to, inner_path, *args, **kwargs)
+
+ def actionFileGet(self, to, inner_path, *args, **kwargs):
+ return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs)
+
+ def actionFileWrite(self, to, inner_path, *args, **kwargs):
+ return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs)
+
+ def actionFileDelete(self, to, inner_path, *args, **kwargs):
+ return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs)
+
+ def actionFileRules(self, to, inner_path, *args, **kwargs):
+ return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs)
+
+ # Add support merger sites for file commands with privatekey parameter
+ def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs):
+ func = getattr(super(UiWebsocketPlugin, self), func_name)
+ if inner_path.startswith("merged-"):
+ merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
+ merged_site = self.server.sites.get(merged_address)
+
+ # Set the same cert for merged site
+ merger_cert = self.user.getSiteData(self.site.address).get("cert")
+ if merger_cert:
+ self.user.setCert(merged_address, merger_cert)
+
+ site_before = self.site # Save to be able to change it back after we ran the command
+ self.site = merged_site # Change the site to the merged one
+ try:
+ back = func(to, privatekey, merged_inner_path, *args, **kwargs)
+ finally:
+ self.site = site_before # Change back to original site
+ return back
+ else:
+ return func(to, privatekey, inner_path, *args, **kwargs)
+
+ def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
+ return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs)
+
+ def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
+ return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs)
+
+ def actionPermissionAdd(self, to, permission):
+ super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
+ self.site.storage.rebuildDb()
+
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+ # Allow to load merged site files using /merged-ZeroMe/address/file.jpg
+ def parsePath(self, path):
+ path_parts = super(UiRequestPlugin, self).parsePath(path)
+ if "merged-" not in path: # Optimization
+ return path_parts
+ path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"])
+ return path_parts
+
+
+@PluginManager.registerTo("SiteStorage")
+class SiteStoragePlugin(object):
+ # Also rebuild from merged sites
+ def getDbFiles(self):
+ merger_types = merger_db.get(self.site.address)
+
+ # First return the site's own db files
+ for item in super(SiteStoragePlugin, self).getDbFiles():
+ yield item
+
+ # Not a merger site, that's all
+ if not merger_types:
+ raise StopIteration
+
+ merged_sites = [
+ site_manager.sites[address]
+ for address, merged_type in merged_db.iteritems()
+ if merged_type in merger_types
+ ]
+ for merged_site in merged_sites:
+ merged_type = merged_db[merged_site.address]
+ for content_inner_path, content in merged_site.content_manager.contents.iteritems():
+ # content.json file itself
+ if merged_site.storage.isFile(content_inner_path): # Missing content.json file
+ content_path = self.getPath("merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path))
+ yield content_path, merged_site.storage.open(content_inner_path)
+ else:
+ merged_site.log.error("[MISSING] %s" % content_inner_path)
+ # Data files in content.json
+ content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site
+ for file_relative_path in content["files"].keys():
+ if not file_relative_path.endswith(".json"):
+ continue # We only interesed in json files
+ file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir
+ file_inner_path = file_inner_path.strip("/") # Strip leading /
+ if merged_site.storage.isFile(file_inner_path):
+ file_path = self.getPath("merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path))
+ yield file_path, merged_site.storage.open(file_inner_path)
+ else:
+ merged_site.log.error("[MISSING] %s" % file_inner_path)
+
+ # Also notice merger sites on a merged site file change
+ def onUpdated(self, inner_path, file=None):
+ super(SiteStoragePlugin, self).onUpdated(inner_path, file)
+
+ merged_type = merged_db.get(self.site.address)
+
+ for merger_site in merged_to_merger.get(self.site.address, []):
+ if merger_site.address == self.site.address: # Avoid infinite loop
+ continue
+ virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path)
+ if inner_path.endswith(".json"):
+ if file is not None:
+ merger_site.storage.onUpdated(virtual_path, file=file)
+ else:
+ merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path))
+ else:
+ merger_site.storage.onUpdated(virtual_path)
+
+ # Send the event to merger site's websocket
+ for ws in merger_site.websockets:
+ ws.event("siteChanged", self.site, {"event": ["file_done", virtual_path]})
+
+
+@PluginManager.registerTo("Site")
+class SitePlugin(object):
+ def fileDone(self, inner_path):
+ super(SitePlugin, self).fileDone(inner_path)
+
+ merged_type = merged_db.get(self.address)
+ virtual_path = "merged-%s/%s/%s" % (merged_type, self.address, inner_path)
+
+ for merger_site in merged_to_merger.get(self.address, []):
+ merger_site.fileDone(virtual_path)
+
+ def fileFailed(self, inner_path):
+ super(SitePlugin, self).fileFailed(inner_path)
+
+ merged_type = merged_db.get(self.address)
+ virtual_path = "merged-%s/%s/%s" % (merged_type, self.address, inner_path)
+
+ for merger_site in merged_to_merger.get(self.address, []):
+ merger_site.fileFailed(virtual_path)
+
+
+@PluginManager.registerTo("SiteManager")
+class SiteManagerPlugin(object):
+ # Update merger site for site types
+ def updateMergerSites(self):
+ global merger_db, merged_db, merged_to_merger, site_manager
+ self.log.debug("Update merger sites")
+ merger_db = {}
+ merged_db = {}
+ merged_to_merger = {}
+ site_manager = self
+ for site in self.sites.itervalues():
+ # Update merger sites
+ for permission in site.settings["permissions"]:
+ if not permission.startswith("Merger:"):
+ continue
+ merger_type = permission.replace("Merger:", "")
+ if site.address not in merger_db:
+ merger_db[site.address] = []
+ merger_db[site.address].append(merger_type)
+ site_manager.sites[site.address] = site
+
+ # Update merged sites
+ merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
+ if merged_type:
+ merged_db[site.address] = merged_type
+
+ # Update merged to merger
+ if merged_type:
+ for merger_site in self.sites.itervalues():
+ if "Merger:" + merged_type in merger_site.settings["permissions"]:
+ if site.address not in merged_to_merger:
+ merged_to_merger[site.address] = []
+ merged_to_merger[site.address].append(merger_site)
+
+
+
+ def load(self, *args, **kwags):
+ super(SiteManagerPlugin, self).load(*args, **kwags)
+ self.updateMergerSites()
+
+ def save(self, *args, **kwags):
+ super(SiteManagerPlugin, self).save(*args, **kwags)
+ self.updateMergerSites()
diff --git a/plugins/MergerSite/__init__.py b/plugins/MergerSite/__init__.py
new file mode 100644
index 00000000..f1f3412c
--- /dev/null
+++ b/plugins/MergerSite/__init__.py
@@ -0,0 +1 @@
+import MergerSitePlugin
\ No newline at end of file