Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
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()