Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
at main 399 lines 18 kB view raw
1import re 2import time 3import copy 4import os 5 6from Plugin import PluginManager 7from Translate import Translate 8from util import RateLimit 9from util import helper 10from util.Flag import flag 11from Debug import Debug 12try: 13 import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible 14except Exception: 15 pass 16 17if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads 18 merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]} 19 merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...} 20 merged_to_merger = {} # {address: [site1, site2, ...]} cache 21 site_manager = None # Site manager for merger sites 22 23 24plugin_dir = os.path.dirname(__file__) 25 26if "_" not in locals(): 27 _ = Translate(plugin_dir + "/languages/") 28 29 30# Check if the site has permission to this merger site 31def checkMergerPath(address, inner_path): 32 merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path) 33 if merged_match: 34 merger_type = merged_match.group(1) 35 # Check if merged site is allowed to include other sites 36 if merger_type in merger_db.get(address, []): 37 # Check if included site allows to include 38 merged_address = merged_match.group(2) 39 if merged_db.get(merged_address) == merger_type: 40 inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path) 41 return merged_address, inner_path 42 else: 43 raise Exception( 44 "Merger site (%s) does not have permission for merged site: %s (%s)" % 45 (merger_type, merged_address, merged_db.get(merged_address)) 46 ) 47 else: 48 raise Exception("No merger (%s) permission to load: <br>%s (%s not in %s)" % ( 49 address, inner_path, merger_type, merger_db.get(address, [])) 50 ) 51 else: 52 raise Exception("Invalid merger path: %s" % inner_path) 53 54 55@PluginManager.registerTo("UiWebsocket") 56class UiWebsocketPlugin(object): 57 # Download new site 58 def actionMergerSiteAdd(self, to, addresses): 59 if type(addresses) != list: 60 # Single site add 61 addresses = [addresses] 62 # Check if the site has merger permission 63 merger_types = merger_db.get(self.site.address) 64 if not merger_types: 65 return self.response(to, {"error": "Not a merger site"}) 66 67 if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1: 68 # Without confirmation if only one site address and not called in last 10 sec 69 self.cbMergerSiteAdd(to, addresses) 70 else: 71 self.cmd( 72 "confirm", 73 [_["Add <b>%s</b> new site?"] % len(addresses), "Add"], 74 lambda res: self.cbMergerSiteAdd(to, addresses) 75 ) 76 self.response(to, "ok") 77 78 # Callback of adding new site confirmation 79 def cbMergerSiteAdd(self, to, addresses): 80 added = 0 81 for address in addresses: 82 try: 83 site_manager.need(address) 84 added += 1 85 except Exception as err: 86 self.cmd("notification", ["error", _["Adding <b>%s</b> failed: %s"] % (address, err)]) 87 if added: 88 self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000]) 89 RateLimit.called(self.site.address + "-MergerSiteAdd") 90 site_manager.updateMergerSites() 91 92 # Delete a merged site 93 @flag.no_multiuser 94 def actionMergerSiteDelete(self, to, address): 95 site = self.server.sites.get(address) 96 if not site: 97 return self.response(to, {"error": "No site found: %s" % address}) 98 99 merger_types = merger_db.get(self.site.address) 100 if not merger_types: 101 return self.response(to, {"error": "Not a merger site"}) 102 if merged_db.get(address) not in merger_types: 103 return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)}) 104 105 self.cmd("notification", ["done", _["Site deleted: <b>%s</b>"] % address, 5000]) 106 self.response(to, "ok") 107 108 # Lists merged sites 109 def actionMergerSiteList(self, to, query_site_info=False): 110 merger_types = merger_db.get(self.site.address) 111 ret = {} 112 if not merger_types: 113 return self.response(to, {"error": "Not a merger site"}) 114 for address, merged_type in merged_db.items(): 115 if merged_type not in merger_types: 116 continue # Site not for us 117 if query_site_info: 118 site = self.server.sites.get(address) 119 ret[address] = self.formatSiteInfo(site, create_user=False) 120 else: 121 ret[address] = merged_type 122 self.response(to, ret) 123 124 def hasSitePermission(self, address, *args, **kwargs): 125 if super(UiWebsocketPlugin, self).hasSitePermission(address, *args, **kwargs): 126 return True 127 else: 128 if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]: 129 return True 130 else: 131 return False 132 133 # Add support merger sites for file commands 134 def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs): 135 if inner_path.startswith("merged-"): 136 merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) 137 138 # Set the same cert for merged site 139 merger_cert = self.user.getSiteData(self.site.address).get("cert") 140 if merger_cert and self.user.getSiteData(merged_address).get("cert") != merger_cert: 141 self.user.setCert(merged_address, merger_cert) 142 143 req_self = copy.copy(self) 144 req_self.site = self.server.sites.get(merged_address) # Change the site to the merged one 145 146 func = getattr(super(UiWebsocketPlugin, req_self), func_name) 147 return func(to, merged_inner_path, *args, **kwargs) 148 else: 149 func = getattr(super(UiWebsocketPlugin, self), func_name) 150 return func(to, inner_path, *args, **kwargs) 151 152 def actionFileList(self, to, inner_path, *args, **kwargs): 153 return self.mergerFuncWrapper("actionFileList", to, inner_path, *args, **kwargs) 154 155 def actionDirList(self, to, inner_path, *args, **kwargs): 156 return self.mergerFuncWrapper("actionDirList", to, inner_path, *args, **kwargs) 157 158 def actionFileGet(self, to, inner_path, *args, **kwargs): 159 return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs) 160 161 def actionFileWrite(self, to, inner_path, *args, **kwargs): 162 return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs) 163 164 def actionFileDelete(self, to, inner_path, *args, **kwargs): 165 return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs) 166 167 def actionFileRules(self, to, inner_path, *args, **kwargs): 168 return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs) 169 170 def actionFileNeed(self, to, inner_path, *args, **kwargs): 171 return self.mergerFuncWrapper("actionFileNeed", to, inner_path, *args, **kwargs) 172 173 def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs): 174 return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs) 175 176 def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs): 177 return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs) 178 179 def actionBigfileUploadInit(self, to, inner_path, *args, **kwargs): 180 back = self.mergerFuncWrapper("actionBigfileUploadInit", to, inner_path, *args, **kwargs) 181 if inner_path.startswith("merged-"): 182 merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) 183 back["inner_path"] = "merged-%s/%s/%s" % (merged_db[merged_address], merged_address, back["inner_path"]) 184 return back 185 186 # Add support merger sites for file commands with privatekey parameter 187 def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs): 188 func = getattr(super(UiWebsocketPlugin, self), func_name) 189 if inner_path.startswith("merged-"): 190 merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) 191 merged_site = self.server.sites.get(merged_address) 192 193 # Set the same cert for merged site 194 merger_cert = self.user.getSiteData(self.site.address).get("cert") 195 if merger_cert: 196 self.user.setCert(merged_address, merger_cert) 197 198 site_before = self.site # Save to be able to change it back after we ran the command 199 self.site = merged_site # Change the site to the merged one 200 try: 201 back = func(to, privatekey, merged_inner_path, *args, **kwargs) 202 finally: 203 self.site = site_before # Change back to original site 204 return back 205 else: 206 return func(to, privatekey, inner_path, *args, **kwargs) 207 208 def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): 209 return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs) 210 211 def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): 212 return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs) 213 214 def actionPermissionAdd(self, to, permission): 215 super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission) 216 if permission.startswith("Merger"): 217 self.site.storage.rebuildDb() 218 219 def actionPermissionDetails(self, to, permission): 220 if not permission.startswith("Merger"): 221 return super(UiWebsocketPlugin, self).actionPermissionDetails(to, permission) 222 223 merger_type = permission.replace("Merger:", "") 224 if not re.match("^[A-Za-z0-9-]+$", merger_type): 225 raise Exception("Invalid merger_type: %s" % merger_type) 226 merged_sites = [] 227 for address, merged_type in merged_db.items(): 228 if merged_type != merger_type: 229 continue 230 site = self.server.sites.get(address) 231 try: 232 merged_sites.append(site.content_manager.contents.get("content.json").get("title", address)) 233 except Exception: 234 merged_sites.append(address) 235 236 details = _["Read and write permissions to sites with merged type of <b>%s</b> "] % merger_type 237 details += _["(%s sites)"] % len(merged_sites) 238 details += "<div style='white-space: normal; max-width: 400px'>%s</div>" % ", ".join(merged_sites) 239 self.response(to, details) 240 241 242@PluginManager.registerTo("UiRequest") 243class UiRequestPlugin(object): 244 # Allow to load merged site files using /merged-ZeroMe/address/file.jpg 245 def parsePath(self, path): 246 path_parts = super(UiRequestPlugin, self).parsePath(path) 247 if "merged-" not in path: # Optimization 248 return path_parts 249 path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"]) 250 return path_parts 251 252 253@PluginManager.registerTo("SiteStorage") 254class SiteStoragePlugin(object): 255 # Also rebuild from merged sites 256 def getDbFiles(self): 257 merger_types = merger_db.get(self.site.address) 258 259 # First return the site's own db files 260 for item in super(SiteStoragePlugin, self).getDbFiles(): 261 yield item 262 263 # Not a merger site, that's all 264 if not merger_types: 265 return 266 267 merged_sites = [ 268 site_manager.sites[address] 269 for address, merged_type in merged_db.items() 270 if merged_type in merger_types 271 ] 272 found = 0 273 for merged_site in merged_sites: 274 self.log.debug("Loading merged site: %s" % merged_site) 275 merged_type = merged_db[merged_site.address] 276 for content_inner_path, content in merged_site.content_manager.contents.items(): 277 # content.json file itself 278 if merged_site.storage.isFile(content_inner_path): # Missing content.json file 279 merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path) 280 yield merged_inner_path, merged_site.storage.getPath(content_inner_path) 281 else: 282 merged_site.log.error("[MISSING] %s" % content_inner_path) 283 # Data files in content.json 284 content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site 285 for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()): 286 if not file_relative_path.endswith(".json"): 287 continue # We only interesed in json files 288 file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir 289 file_inner_path = file_inner_path.strip("/") # Strip leading / 290 if merged_site.storage.isFile(file_inner_path): 291 merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path) 292 yield merged_inner_path, merged_site.storage.getPath(file_inner_path) 293 else: 294 merged_site.log.error("[MISSING] %s" % file_inner_path) 295 found += 1 296 if found % 100 == 0: 297 time.sleep(0.001) # Context switch to avoid UI block 298 299 # Also notice merger sites on a merged site file change 300 def onUpdated(self, inner_path, file=None): 301 super(SiteStoragePlugin, self).onUpdated(inner_path, file) 302 303 merged_type = merged_db.get(self.site.address) 304 305 for merger_site in merged_to_merger.get(self.site.address, []): 306 if merger_site.address == self.site.address: # Avoid infinite loop 307 continue 308 virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path) 309 if inner_path.endswith(".json"): 310 if file is not None: 311 merger_site.storage.onUpdated(virtual_path, file=file) 312 else: 313 merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path)) 314 else: 315 merger_site.storage.onUpdated(virtual_path) 316 317 318@PluginManager.registerTo("Site") 319class SitePlugin(object): 320 def fileDone(self, inner_path): 321 super(SitePlugin, self).fileDone(inner_path) 322 323 for merger_site in merged_to_merger.get(self.address, []): 324 if merger_site.address == self.address: 325 continue 326 for ws in merger_site.websockets: 327 ws.event("siteChanged", self, {"event": ["file_done", inner_path]}) 328 329 def fileFailed(self, inner_path): 330 super(SitePlugin, self).fileFailed(inner_path) 331 332 for merger_site in merged_to_merger.get(self.address, []): 333 if merger_site.address == self.address: 334 continue 335 for ws in merger_site.websockets: 336 ws.event("siteChanged", self, {"event": ["file_failed", inner_path]}) 337 338 339@PluginManager.registerTo("SiteManager") 340class SiteManagerPlugin(object): 341 # Update merger site for site types 342 def updateMergerSites(self): 343 global merger_db, merged_db, merged_to_merger, site_manager 344 s = time.time() 345 merger_db_new = {} 346 merged_db_new = {} 347 merged_to_merger_new = {} 348 site_manager = self 349 if not self.sites: 350 return 351 for site in self.sites.values(): 352 # Update merged sites 353 try: 354 merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type") 355 except Exception as err: 356 self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err))) 357 continue 358 if merged_type: 359 merged_db_new[site.address] = merged_type 360 361 # Update merger sites 362 for permission in site.settings["permissions"]: 363 if not permission.startswith("Merger:"): 364 continue 365 if merged_type: 366 self.log.error( 367 "Removing permission %s from %s: Merger and merged at the same time." % 368 (permission, site.address) 369 ) 370 site.settings["permissions"].remove(permission) 371 continue 372 merger_type = permission.replace("Merger:", "") 373 if site.address not in merger_db_new: 374 merger_db_new[site.address] = [] 375 merger_db_new[site.address].append(merger_type) 376 site_manager.sites[site.address] = site 377 378 # Update merged to merger 379 if merged_type: 380 for merger_site in self.sites.values(): 381 if "Merger:" + merged_type in merger_site.settings["permissions"]: 382 if site.address not in merged_to_merger_new: 383 merged_to_merger_new[site.address] = [] 384 merged_to_merger_new[site.address].append(merger_site) 385 386 # Update globals 387 merger_db = merger_db_new 388 merged_db = merged_db_new 389 merged_to_merger = merged_to_merger_new 390 391 self.log.debug("Updated merger sites in %.3fs" % (time.time() - s)) 392 393 def load(self, *args, **kwags): 394 super(SiteManagerPlugin, self).load(*args, **kwags) 395 self.updateMergerSites() 396 397 def saveDelayed(self, *args, **kwags): 398 super(SiteManagerPlugin, self).saveDelayed(*args, **kwags) 399 self.updateMergerSites()