SidebarPlugin.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. import re
  2. import os
  3. import html
  4. import sys
  5. import math
  6. import time
  7. import json
  8. import io
  9. import urllib
  10. import urllib.parse
  11. import gevent
  12. import util
  13. import main
  14. from Config import config
  15. from Plugin import PluginManager
  16. from Debug import Debug
  17. from Translate import Translate
  18. from util import helper
  19. from util.Flag import flag
  20. from .ZipStream import ZipStream
  21. plugin_dir = os.path.dirname(__file__)
  22. media_dir = plugin_dir + "/media"
  23. loc_cache = {}
  24. if "_" not in locals():
  25. _ = Translate(plugin_dir + "/languages/")
  26. @PluginManager.registerTo("UiRequest")
  27. class UiRequestPlugin(object):
  28. # Inject our resources to end of original file streams
  29. def actionUiMedia(self, path):
  30. if path == "/uimedia/all.js" or path == "/uimedia/all.css":
  31. # First yield the original file and header
  32. body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
  33. for part in body_generator:
  34. yield part
  35. # Append our media file to the end
  36. ext = re.match(".*(js|css)$", path).group(1)
  37. plugin_media_file = "%s/all.%s" % (media_dir, ext)
  38. if config.debug:
  39. # If debugging merge *.css to all.css and *.js to all.js
  40. from Debug import DebugMedia
  41. DebugMedia.merge(plugin_media_file)
  42. if ext == "js":
  43. yield _.translateData(open(plugin_media_file).read()).encode("utf8")
  44. else:
  45. for part in self.actionFile(plugin_media_file, send_header=False):
  46. yield part
  47. elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files
  48. file_name = re.match(".*/(.*)", path).group(1)
  49. plugin_media_file = "%s_globe/%s" % (media_dir, file_name)
  50. if config.debug and path.endswith("all.js"):
  51. # If debugging merge *.css to all.css and *.js to all.js
  52. from Debug import DebugMedia
  53. DebugMedia.merge(plugin_media_file)
  54. for part in self.actionFile(plugin_media_file):
  55. yield part
  56. else:
  57. for part in super(UiRequestPlugin, self).actionUiMedia(path):
  58. yield part
  59. def actionZip(self):
  60. address = self.get["address"]
  61. site = self.server.site_manager.get(address)
  62. if not site:
  63. return self.error404("Site not found")
  64. title = site.content_manager.contents.get("content.json", {}).get("title", "")
  65. filename = "%s-backup-%s.zip" % (title, time.strftime("%Y-%m-%d_%H_%M"))
  66. filename_quoted = urllib.parse.quote(filename)
  67. self.sendHeader(content_type="application/zip", extra_headers={'Content-Disposition': 'attachment; filename="%s"' % filename_quoted})
  68. return self.streamZip(site.storage.getPath("."))
  69. def streamZip(self, dir_path):
  70. zs = ZipStream(dir_path)
  71. while 1:
  72. data = zs.read()
  73. if not data:
  74. break
  75. yield data
  76. @PluginManager.registerTo("UiWebsocket")
  77. class UiWebsocketPlugin(object):
  78. def sidebarRenderPeerStats(self, body, site):
  79. connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected])
  80. connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")])
  81. onion = len([peer_id for peer_id in list(site.peers.keys()) if ".onion" in peer_id])
  82. local = len([peer for peer in list(site.peers.values()) if helper.isPrivateIp(peer.ip)])
  83. peers_total = len(site.peers)
  84. # Add myself
  85. if site.isServing():
  86. peers_total += 1
  87. if any(site.connection_server.port_opened.values()):
  88. connectable += 1
  89. if site.connection_server.tor_manager.start_onions:
  90. onion += 1
  91. if peers_total:
  92. percent_connected = float(connected) / peers_total
  93. percent_connectable = float(connectable) / peers_total
  94. percent_onion = float(onion) / peers_total
  95. else:
  96. percent_connectable = percent_connected = percent_onion = 0
  97. if local:
  98. local_html = _("<li class='color-yellow'><span>{_[Local]}:</span><b>{local}</b></li>")
  99. else:
  100. local_html = ""
  101. peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
  102. self_onion = main.file_server.tor_manager.site_onions.get(site.address, None)
  103. if self_onion is not None:
  104. peer_ips.append(self_onion+'.onion')
  105. peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
  106. copy_link = f'http://127.0.0.1:43110/{site.address}/?zeronet_peers={",".join(peer_ips)}'
  107. body.append(_("""
  108. <li>
  109. <label>
  110. {_[Peers]}
  111. <small class="label-right"><a href='{copy_link}' id='link-copypeers' class='link-right'>{_[Copy to clipboard]}</a></small>
  112. </label>
  113. <ul class='graph'>
  114. <li style='width: 100%' class='total back-black' title="{_[Total peers]}"></li>
  115. <li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Connectable peers]}'></li>
  116. <li style='width: {percent_onion:.0%}' class='connected back-purple' title='{_[Onion]}'></li>
  117. <li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
  118. </ul>
  119. <ul class='graph-legend'>
  120. <li class='color-green'><span>{_[Connected]}:</span><b>{connected}</b></li>
  121. <li class='color-blue'><span>{_[Connectable]}:</span><b>{connectable}</b></li>
  122. <li class='color-purple'><span>{_[Onion]}:</span><b>{onion}</b></li>
  123. {local_html}
  124. <li class='color-black'><span>{_[Total]}:</span><b>{peers_total}</b></li>
  125. </ul>
  126. </li>
  127. """.replace("{local_html}", local_html)))
  128. def sidebarRenderTransferStats(self, body, site):
  129. recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
  130. sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
  131. transfer_total = recv + sent
  132. if transfer_total:
  133. percent_recv = recv / transfer_total
  134. percent_sent = sent / transfer_total
  135. else:
  136. percent_recv = 0.5
  137. percent_sent = 0.5
  138. body.append(_("""
  139. <li>
  140. <label>{_[Data transfer]}</label>
  141. <ul class='graph graph-stacked'>
  142. <li style='width: {percent_recv:.0%}' class='received back-yellow' title="{_[Received bytes]}"></li>
  143. <li style='width: {percent_sent:.0%}' class='sent back-green' title="{_[Sent bytes]}"></li>
  144. </ul>
  145. <ul class='graph-legend'>
  146. <li class='color-yellow'><span>{_[Received]}:</span><b>{recv:.2f}MB</b></li>
  147. <li class='color-green'<span>{_[Sent]}:</span><b>{sent:.2f}MB</b></li>
  148. </ul>
  149. </li>
  150. """))
  151. def sidebarRenderFileStats(self, body, site):
  152. body.append(_("""
  153. <li>
  154. <label>
  155. {_[Files]}
  156. <a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
  157. <small class="label-right">
  158. <a href='#Site+directory' id='link-directory' class='link-right'>{_[Open site directory]}</a>
  159. <a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
  160. </small>
  161. </label>
  162. <ul class='graph graph-stacked'>
  163. """))
  164. extensions = (
  165. ("html", "yellow"),
  166. ("css", "orange"),
  167. ("js", "purple"),
  168. ("Image", "green"),
  169. ("json", "darkblue"),
  170. ("User data", "blue"),
  171. ("Other", "white"),
  172. ("Total", "black")
  173. )
  174. # Collect stats
  175. size_filetypes = {}
  176. size_total = 0
  177. contents = site.content_manager.listContents() # Without user files
  178. for inner_path in contents:
  179. content = site.content_manager.contents[inner_path]
  180. if "files" not in content or content["files"] is None:
  181. continue
  182. for file_name, file_details in list(content["files"].items()):
  183. size_total += file_details["size"]
  184. ext = file_name.split(".")[-1]
  185. size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
  186. # Get user file sizes
  187. size_user_content = site.content_manager.contents.execute(
  188. "SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
  189. {"not__inner_path": contents}
  190. ).fetchone()["size"]
  191. if not size_user_content:
  192. size_user_content = 0
  193. size_filetypes["User data"] = size_user_content
  194. size_total += size_user_content
  195. # The missing difference is content.json sizes
  196. if "json" in size_filetypes:
  197. size_filetypes["json"] += max(0, site.settings["size"] - size_total)
  198. size_total = size_other = site.settings["size"]
  199. # Bar
  200. for extension, color in extensions:
  201. if extension == "Total":
  202. continue
  203. if extension == "Other":
  204. size = max(0, size_other)
  205. elif extension == "Image":
  206. size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
  207. size_other -= size
  208. else:
  209. size = size_filetypes.get(extension, 0)
  210. size_other -= size
  211. if size_total == 0:
  212. percent = 0
  213. else:
  214. percent = 100 * (float(size) / size_total)
  215. percent = math.floor(percent * 100) / 100 # Floor to 2 digits
  216. body.append(
  217. """<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
  218. (percent, _[extension], color, _[extension])
  219. )
  220. # Legend
  221. body.append("</ul><ul class='graph-legend'>")
  222. for extension, color in extensions:
  223. if extension == "Other":
  224. size = max(0, size_other)
  225. elif extension == "Image":
  226. size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
  227. elif extension == "Total":
  228. size = size_total
  229. else:
  230. size = size_filetypes.get(extension, 0)
  231. if extension == "js":
  232. title = "javascript"
  233. else:
  234. title = extension
  235. if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
  236. size_formatted = "%.0fMB" % (size / 1024 / 1024)
  237. else:
  238. size_formatted = "%.0fkB" % (size / 1024)
  239. body.append("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
  240. body.append("</ul></li>")
  241. def sidebarRenderSizeLimit(self, body, site):
  242. free_space = helper.getFreeSpace() / 1024 / 1024
  243. size = float(site.settings["size"]) / 1024 / 1024
  244. size_limit = site.getSizeLimit()
  245. percent_used = size / size_limit
  246. body.append(_("""
  247. <li>
  248. <label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,.0f}MB)</small></label>
  249. <input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
  250. <a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
  251. </li>
  252. """))
  253. def sidebarRenderOptionalFileStats(self, body, site):
  254. size_total = float(site.settings["size_optional"])
  255. size_downloaded = float(site.settings["optional_downloaded"])
  256. if not size_total:
  257. return False
  258. percent_downloaded = size_downloaded / size_total
  259. size_formatted_total = size_total / 1024 / 1024
  260. size_formatted_downloaded = size_downloaded / 1024 / 1024
  261. body.append(_("""
  262. <li>
  263. <label>{_[Optional files]}</label>
  264. <ul class='graph'>
  265. <li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
  266. <li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
  267. </ul>
  268. <ul class='graph-legend'>
  269. <li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
  270. <li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
  271. </ul>
  272. </li>
  273. """))
  274. return True
  275. def sidebarRenderOptionalFileSettings(self, body, site):
  276. if self.site.settings.get("autodownloadoptional"):
  277. checked = "checked='checked'"
  278. else:
  279. checked = ""
  280. body.append(_("""
  281. <li>
  282. <label>{_[Help distribute added optional files]}</label>
  283. <input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
  284. """))
  285. if hasattr(config, "autodownload_bigfile_size_limit"):
  286. autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
  287. body.append(_("""
  288. <div class='settings-autodownloadoptional'>
  289. <label>{_[Auto download big file size limit]}</label>
  290. <input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
  291. <a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
  292. <a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
  293. </div>
  294. """))
  295. body.append("</li>")
  296. def sidebarRenderBadFiles(self, body, site):
  297. body.append(_("""
  298. <li>
  299. <label>{_[Needs to be updated]}:</label>
  300. <ul class='filelist'>
  301. """))
  302. i = 0
  303. for bad_file, tries in site.bad_files.items():
  304. i += 1
  305. body.append(_("""<li class='color-red' title="{bad_file_path} ({tries})">{bad_filename}</li>""", {
  306. "bad_file_path": bad_file,
  307. "bad_filename": helper.getFilename(bad_file),
  308. "tries": _.pluralize(tries, "{} try", "{} tries")
  309. }))
  310. if i > 30:
  311. break
  312. if len(site.bad_files) > 30:
  313. num_bad_files = len(site.bad_files) - 30
  314. body.append(_("""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
  315. body.append("""
  316. </ul>
  317. </li>
  318. """)
  319. def sidebarRenderDbOptions(self, body, site):
  320. if site.storage.db:
  321. inner_path = site.storage.getInnerPath(site.storage.db.db_path)
  322. size = float(site.storage.getSize(inner_path)) / 1024
  323. feeds = len(site.storage.db.schema.get("feeds", {}))
  324. else:
  325. inner_path = _["No database found"]
  326. size = 0.0
  327. feeds = 0
  328. body.append(_("""
  329. <li>
  330. <label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
  331. <div class='flex'>
  332. <input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
  333. <a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
  334. <a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
  335. </div>
  336. </li>
  337. """, nested=True))
  338. def sidebarRenderIdentity(self, body, site):
  339. auth_address = self.user.getAuthAddress(self.site.address, create=False)
  340. rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
  341. if rules and rules.get("max_size"):
  342. quota = rules["max_size"] / 1024
  343. try:
  344. content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
  345. used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())])
  346. except:
  347. used = 0
  348. used = used / 1024
  349. else:
  350. quota = used = 0
  351. body.append(_("""
  352. <li>
  353. <label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
  354. <div class='flex'>
  355. <span class='input text disabled'>{auth_address}</span>
  356. <a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
  357. </div>
  358. </li>
  359. """))
  360. def sidebarRenderControls(self, body, site):
  361. auth_address = self.user.getAuthAddress(self.site.address, create=False)
  362. if self.site.settings["serving"]:
  363. class_pause = ""
  364. class_resume = "hidden"
  365. else:
  366. class_pause = "hidden"
  367. class_resume = ""
  368. dashboard = config.homepage
  369. dsite = self.user.sites.get(dashboard, None)
  370. if not dsite:
  371. print('No dashboard found, cannot favourite')
  372. class_favourite = "hidden"
  373. class_unfavourite = "hidden"
  374. elif dsite.get('sittings', {}).get('favorite_sites', {}).get(self.site.address, False):
  375. class_favourite = ""
  376. class_unfavourite = "hidden"
  377. else:
  378. class_favourite = "hidden"
  379. class_unfavourite = ""
  380. body.append(_("""
  381. <li>
  382. <label>{_[Site control]}</label>
  383. <a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
  384. <a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
  385. <a href='#Favourite' class='button {class_favourite}' id='button-favourite'>{_[Favourite]}</a>
  386. <a href='#Unfavourite' class='button {class_unfavourite}' id='button-unfavourite'>{_[Unfavourite]}</a>
  387. <a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
  388. <a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
  389. </li>
  390. """))
  391. site_address = self.site.address
  392. body.append(_("""
  393. <li>
  394. <label>{_[Site address]}</label><br>
  395. <div class='flex'>
  396. <span class='input text disabled'>{site_address}</span>
  397. </div>
  398. </li>
  399. """))
  400. donate_generic = site.content_manager.contents.get("content.json", {}).get("donate", None) or site.content_manager.contents.get("content.json", {}).get("donate-generic", None)
  401. donate_btc = site.content_manager.contents.get("content.json", {}).get("donate-btc", None)
  402. donate_xmr = site.content_manager.contents.get("content.json", {}).get("donate-xmr", None)
  403. donate_ada = site.content_manager.contents.get("content.json", {}).get("donate-ada", None)
  404. donate_enabled = bool(donate_generic or donate_btc or donate_xmr or donate_ada)
  405. if donate_enabled:
  406. body.append(_("""
  407. <li>
  408. <label>{_[Donate]}</label><br>
  409. """))
  410. if donate_generic:
  411. body.append(_("""
  412. <div class='flex'>
  413. {donate_generic}
  414. </div>
  415. """))
  416. if donate_btc:
  417. body.append(_("""
  418. <div class='flex'>
  419. <span style="font-size:90%">{donate_btc}</span><br/>
  420. </div>
  421. <div class='flex'>
  422. <a href='bitcoin:{donate_btc}' class='button'>{_[Donate BTC]}</a>
  423. </div>
  424. """))
  425. if donate_xmr:
  426. body.append(_("""
  427. <div class='flex'>
  428. <span style="font-size:90%">{donate_xmr}</span><br/>
  429. </div>
  430. <div class='flex'>
  431. <a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>
  432. </div>
  433. """))
  434. if donate_ada:
  435. body.append(_("""
  436. <div class='flex'>
  437. <span style="font-size:90%">{donate_ada}</span><br/>
  438. </div>
  439. <div class='flex'>
  440. <a href='web+cardano:{donate_ada}' class='button'>{_[Donate Ada/Cardano]}</a>
  441. </div>
  442. """))
  443. if donate_enabled:
  444. body.append(_("""
  445. </li>
  446. """))
  447. def sidebarRenderOwnedCheckbox(self, body, site):
  448. if self.site.settings["own"]:
  449. checked = "checked='checked'"
  450. else:
  451. checked = ""
  452. body.append(_("""
  453. <h2 class='owned-title'>{_[This is my site]}</h2>
  454. <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
  455. """))
  456. def sidebarRenderOwnSettings(self, body, site):
  457. title = site.content_manager.contents.get("content.json", {}).get("title", "")
  458. description = site.content_manager.contents.get("content.json", {}).get("description", "")
  459. body.append(_("""
  460. <li>
  461. <label for='settings-title'>{_[Site title]}</label>
  462. <input type='text' class='text' value="{title}" id='settings-title'/>
  463. </li>
  464. <li>
  465. <label for='settings-description'>{_[Site description]}</label>
  466. <input type='text' class='text' value="{description}" id='settings-description'/>
  467. </li>
  468. <li>
  469. <a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
  470. </li>
  471. """))
  472. def sidebarRenderContents(self, body, site):
  473. has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
  474. if has_privatekey:
  475. tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
  476. else:
  477. tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
  478. body.append(_("""
  479. <li>
  480. <label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
  481. """.replace("{tag_privatekey}", tag_privatekey)))
  482. # Choose content you want to sign
  483. body.append(_("""
  484. <div class='flex'>
  485. <input type='text' class='text' value="content.json" id='input-contents'/>
  486. <a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
  487. <a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
  488. </div>
  489. """))
  490. contents = ["content.json"]
  491. contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
  492. body.append(_("<div class='contents'>{_[Choose]}: "))
  493. for content in contents:
  494. body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
  495. body.append("</div>")
  496. body.append("</li>")
  497. @flag.admin
  498. def actionSidebarGetHtmlTag(self, to):
  499. site = self.site
  500. body = []
  501. body.append("<div>")
  502. body.append("<a href='#Close' class='close'>&times;</a>")
  503. body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
  504. body.append("<div class='globe loading'></div>")
  505. body.append("<ul class='fields'>")
  506. self.sidebarRenderPeerStats(body, site)
  507. self.sidebarRenderTransferStats(body, site)
  508. self.sidebarRenderFileStats(body, site)
  509. self.sidebarRenderSizeLimit(body, site)
  510. has_optional = self.sidebarRenderOptionalFileStats(body, site)
  511. if has_optional:
  512. self.sidebarRenderOptionalFileSettings(body, site)
  513. self.sidebarRenderDbOptions(body, site)
  514. self.sidebarRenderIdentity(body, site)
  515. self.sidebarRenderControls(body, site)
  516. if site.bad_files:
  517. self.sidebarRenderBadFiles(body, site)
  518. self.sidebarRenderOwnedCheckbox(body, site)
  519. body.append("<div class='settings-owned'>")
  520. self.sidebarRenderOwnSettings(body, site)
  521. self.sidebarRenderContents(body, site)
  522. body.append("</div>")
  523. body.append("</ul>")
  524. body.append("</div>")
  525. body.append("<div class='menu template'>")
  526. body.append("<a href='#'' class='menu-item template'>Template</a>")
  527. body.append("</div>")
  528. self.response(to, "".join(body))
  529. def downloadGeoLiteDb(self, db_path):
  530. import gzip
  531. import shutil
  532. from util import helper
  533. if config.offline or config.tor == 'always':
  534. return False
  535. self.log.info("Downloading GeoLite2 City database...")
  536. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
  537. db_urls = [
  538. "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
  539. "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
  540. ]
  541. for db_url in db_urls:
  542. downloadl_err = None
  543. try:
  544. # Download
  545. response = helper.httpRequest(db_url)
  546. data_size = response.getheader('content-length')
  547. data_recv = 0
  548. data = io.BytesIO()
  549. while True:
  550. buff = response.read(1024 * 512)
  551. if not buff:
  552. break
  553. data.write(buff)
  554. data_recv += 1024 * 512
  555. if data_size:
  556. progress = int(float(data_recv) / int(data_size) * 100)
  557. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
  558. self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
  559. data.seek(0)
  560. # Unpack
  561. with gzip.GzipFile(fileobj=data) as gzip_file:
  562. shutil.copyfileobj(gzip_file, open(db_path, "wb"))
  563. self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
  564. time.sleep(2) # Wait for notify animation
  565. self.log.info("GeoLite2 City database is ready at: %s" % db_path)
  566. return True
  567. except Exception as err:
  568. download_err = err
  569. self.log.error("Error downloading %s: %s" % (db_url, err))
  570. pass
  571. self.cmd("progress", [
  572. "geolite-info",
  573. _["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
  574. -100
  575. ])
  576. def getLoc(self, geodb, ip):
  577. global loc_cache
  578. if ip in loc_cache:
  579. return loc_cache[ip]
  580. else:
  581. try:
  582. loc_data = geodb.get(ip)
  583. except:
  584. loc_data = None
  585. if not loc_data or "location" not in loc_data:
  586. loc_cache[ip] = None
  587. return None
  588. loc = {
  589. "lat": loc_data["location"]["latitude"],
  590. "lon": loc_data["location"]["longitude"],
  591. }
  592. if "city" in loc_data:
  593. loc["city"] = loc_data["city"]["names"]["en"]
  594. if "country" in loc_data:
  595. loc["country"] = loc_data["country"]["names"]["en"]
  596. loc_cache[ip] = loc
  597. return loc
  598. @util.Noparallel()
  599. def getGeoipDb(self):
  600. db_name = 'GeoLite2-City.mmdb'
  601. sys_db_paths = []
  602. if sys.platform == "linux":
  603. sys_db_paths += ['/usr/share/GeoIP/' + db_name]
  604. data_dir_db_path = os.path.join(config.data_dir, db_name)
  605. db_paths = sys_db_paths + [data_dir_db_path]
  606. for path in db_paths:
  607. if os.path.isfile(path) and os.path.getsize(path) > 0:
  608. return path
  609. self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
  610. " ".join(db_paths), data_dir_db_path)
  611. if self.downloadGeoLiteDb(data_dir_db_path):
  612. return data_dir_db_path
  613. return None
  614. def getPeerLocations(self, peers):
  615. import maxminddb
  616. db_path = self.getGeoipDb()
  617. if not db_path:
  618. self.log.debug("Not showing peer locations: no GeoIP database")
  619. return False
  620. geodb = maxminddb.open_database(db_path)
  621. peers = list(peers.values())
  622. # Place bars
  623. peer_locations = []
  624. placed = {} # Already placed bars here
  625. for peer in peers:
  626. # Height of bar
  627. if peer.connection and peer.connection.last_ping_delay:
  628. ping = round(peer.connection.last_ping_delay * 1000)
  629. else:
  630. ping = None
  631. loc = self.getLoc(geodb, peer.ip)
  632. if not loc:
  633. continue
  634. # Create position array
  635. lat, lon = loc["lat"], loc["lon"]
  636. latlon = "%s,%s" % (lat, lon)
  637. if latlon in placed and helper.getIpType(peer.ip) == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part
  638. lat += float(128 - int(peer.ip.split(".")[-2])) / 50
  639. lon += float(128 - int(peer.ip.split(".")[-1])) / 50
  640. latlon = "%s,%s" % (lat, lon)
  641. placed[latlon] = True
  642. peer_location = {}
  643. peer_location.update(loc)
  644. peer_location["lat"] = lat
  645. peer_location["lon"] = lon
  646. peer_location["ping"] = ping
  647. peer_locations.append(peer_location)
  648. # Append myself
  649. for ip in self.site.connection_server.ip_external_list:
  650. my_loc = self.getLoc(geodb, ip)
  651. if my_loc:
  652. my_loc["ping"] = 0
  653. peer_locations.append(my_loc)
  654. return peer_locations
  655. @flag.admin
  656. @flag.async_run
  657. def actionSidebarGetPeers(self, to):
  658. try:
  659. peer_locations = self.getPeerLocations(self.site.peers)
  660. globe_data = []
  661. ping_times = [
  662. peer_location["ping"]
  663. for peer_location in peer_locations
  664. if peer_location["ping"]
  665. ]
  666. if ping_times:
  667. ping_avg = sum(ping_times) / float(len(ping_times))
  668. else:
  669. ping_avg = 0
  670. for peer_location in peer_locations:
  671. if peer_location["ping"] == 0: # Me
  672. height = -0.135
  673. elif peer_location["ping"]:
  674. height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
  675. else:
  676. height = -0.03
  677. globe_data += [peer_location["lat"], peer_location["lon"], height]
  678. self.response(to, globe_data)
  679. except Exception as err:
  680. self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
  681. self.response(to, {"error": str(err)})
  682. @flag.admin
  683. @flag.no_multiuser
  684. def actionSiteSetOwned(self, to, owned):
  685. self.site.settings["own"] = bool(owned)
  686. self.site.updateWebsocket(owned=owned)
  687. return "ok"
  688. @flag.admin
  689. @flag.no_multiuser
  690. def actionSiteRecoverPrivatekey(self, to):
  691. from Crypt import CryptBitcoin
  692. site_data = self.user.sites[self.site.address]
  693. if site_data.get("privatekey"):
  694. return {"error": "This site already has saved privated key"}
  695. address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
  696. if not address_index:
  697. return {"error": "No address_index in content.json"}
  698. privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
  699. privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
  700. if privatekey_address == self.site.address:
  701. site_data["privatekey"] = privatekey
  702. self.user.save()
  703. self.site.updateWebsocket(recover_privatekey=True)
  704. return "ok"
  705. else:
  706. return {"error": "Unable to deliver private key for this site from current user's master_seed"}
  707. @flag.admin
  708. @flag.no_multiuser
  709. def actionUserSetSitePrivatekey(self, to, privatekey):
  710. site_data = self.user.sites[self.site.address]
  711. site_data["privatekey"] = privatekey
  712. self.site.updateWebsocket(set_privatekey=bool(privatekey))
  713. self.user.save()
  714. return "ok"
  715. @flag.admin
  716. @flag.no_multiuser
  717. def actionSiteSetAutodownloadoptional(self, to, owned):
  718. self.site.settings["autodownloadoptional"] = bool(owned)
  719. self.site.worker_manager.removeSolvedFileTasks()
  720. @flag.no_multiuser
  721. @flag.admin
  722. def actionDbReload(self, to):
  723. self.site.storage.closeDb()
  724. self.site.storage.getDb()
  725. return self.response(to, "ok")
  726. @flag.no_multiuser
  727. @flag.admin
  728. def actionDbRebuild(self, to):
  729. try:
  730. self.site.storage.rebuildDb()
  731. except Exception as err:
  732. return self.response(to, {"error": str(err)})
  733. return self.response(to, "ok")