SidebarPlugin.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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 not dsite.get('settings', {}).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. import requests
  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 = requests.get(db_url, stream=True)
  546. data_size = response.headers.get('content-length')
  547. if data_size is None:
  548. data.write(response.content)
  549. data_size = int(data_size)
  550. data_recv = 0
  551. data = io.BytesIO()
  552. for buff in response.iter_content(chunk_size=1024 * 512):
  553. data.write(buff)
  554. data_recv += 1024 * 512
  555. progress = int(float(data_recv) / data_size * 100)
  556. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
  557. self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
  558. data.seek(0)
  559. # Unpack
  560. with gzip.GzipFile(fileobj=data) as gzip_file:
  561. shutil.copyfileobj(gzip_file, open(db_path, "wb"))
  562. self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
  563. time.sleep(2) # Wait for notify animation
  564. self.log.info("GeoLite2 City database is ready at: %s" % db_path)
  565. return True
  566. except Exception as err:
  567. download_err = err
  568. self.log.error("Error downloading %s: %s" % (db_url, err))
  569. pass
  570. self.cmd("progress", [
  571. "geolite-info",
  572. _["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
  573. -100
  574. ])
  575. def getLoc(self, geodb, ip):
  576. global loc_cache
  577. if ip in loc_cache:
  578. return loc_cache[ip]
  579. else:
  580. try:
  581. loc_data = geodb.get(ip)
  582. except:
  583. loc_data = None
  584. if not loc_data or "location" not in loc_data:
  585. loc_cache[ip] = None
  586. return None
  587. loc = {
  588. "lat": loc_data["location"]["latitude"],
  589. "lon": loc_data["location"]["longitude"],
  590. }
  591. if "city" in loc_data:
  592. loc["city"] = loc_data["city"]["names"]["en"]
  593. if "country" in loc_data:
  594. loc["country"] = loc_data["country"]["names"]["en"]
  595. loc_cache[ip] = loc
  596. return loc
  597. @util.Noparallel()
  598. def getGeoipDb(self):
  599. db_name = 'GeoLite2-City.mmdb'
  600. sys_db_paths = []
  601. if sys.platform == "linux":
  602. sys_db_paths += ['/usr/share/GeoIP/' + db_name]
  603. data_dir_db_path = os.path.join(config.data_dir, db_name)
  604. db_paths = sys_db_paths + [data_dir_db_path]
  605. for path in db_paths:
  606. if os.path.isfile(path) and os.path.getsize(path) > 0:
  607. return path
  608. self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
  609. " ".join(db_paths), data_dir_db_path)
  610. if self.downloadGeoLiteDb(data_dir_db_path):
  611. return data_dir_db_path
  612. return None
  613. def getPeerLocations(self, peers):
  614. import maxminddb
  615. db_path = self.getGeoipDb()
  616. if not db_path:
  617. self.log.debug("Not showing peer locations: no GeoIP database")
  618. return False
  619. geodb = maxminddb.open_database(db_path)
  620. peers = list(peers.values())
  621. # Place bars
  622. peer_locations = []
  623. placed = {} # Already placed bars here
  624. for peer in peers:
  625. # Height of bar
  626. if peer.connection and peer.connection.last_ping_delay:
  627. ping = round(peer.connection.last_ping_delay * 1000)
  628. else:
  629. ping = None
  630. loc = self.getLoc(geodb, peer.ip)
  631. if not loc:
  632. continue
  633. # Create position array
  634. lat, lon = loc["lat"], loc["lon"]
  635. latlon = "%s,%s" % (lat, lon)
  636. 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
  637. lat += float(128 - int(peer.ip.split(".")[-2])) / 50
  638. lon += float(128 - int(peer.ip.split(".")[-1])) / 50
  639. latlon = "%s,%s" % (lat, lon)
  640. placed[latlon] = True
  641. peer_location = {}
  642. peer_location.update(loc)
  643. peer_location["lat"] = lat
  644. peer_location["lon"] = lon
  645. peer_location["ping"] = ping
  646. peer_locations.append(peer_location)
  647. # Append myself
  648. for ip in self.site.connection_server.ip_external_list:
  649. my_loc = self.getLoc(geodb, ip)
  650. if my_loc:
  651. my_loc["ping"] = 0
  652. peer_locations.append(my_loc)
  653. return peer_locations
  654. @flag.admin
  655. @flag.async_run
  656. def actionSidebarGetPeers(self, to):
  657. try:
  658. peer_locations = self.getPeerLocations(self.site.peers)
  659. globe_data = []
  660. ping_times = [
  661. peer_location["ping"]
  662. for peer_location in peer_locations
  663. if peer_location["ping"]
  664. ]
  665. if ping_times:
  666. ping_avg = sum(ping_times) / float(len(ping_times))
  667. else:
  668. ping_avg = 0
  669. for peer_location in peer_locations:
  670. if peer_location["ping"] == 0: # Me
  671. height = -0.135
  672. elif peer_location["ping"]:
  673. height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
  674. else:
  675. height = -0.03
  676. globe_data += [peer_location["lat"], peer_location["lon"], height]
  677. self.response(to, globe_data)
  678. except Exception as err:
  679. self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
  680. self.response(to, {"error": str(err)})
  681. @flag.admin
  682. @flag.no_multiuser
  683. def actionSiteSetOwned(self, to, owned):
  684. self.site.settings["own"] = bool(owned)
  685. self.site.updateWebsocket(owned=owned)
  686. return "ok"
  687. @flag.admin
  688. @flag.no_multiuser
  689. def actionSiteRecoverPrivatekey(self, to):
  690. from Crypt import CryptBitcoin
  691. site_data = self.user.sites[self.site.address]
  692. if site_data.get("privatekey"):
  693. return {"error": "This site already has saved privated key"}
  694. address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
  695. if not address_index:
  696. return {"error": "No address_index in content.json"}
  697. privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
  698. privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
  699. if privatekey_address == self.site.address:
  700. site_data["privatekey"] = privatekey
  701. self.user.save()
  702. self.site.updateWebsocket(recover_privatekey=True)
  703. return "ok"
  704. else:
  705. return {"error": "Unable to deliver private key for this site from current user's master_seed"}
  706. @flag.admin
  707. @flag.no_multiuser
  708. def actionUserSetSitePrivatekey(self, to, privatekey):
  709. site_data = self.user.sites[self.site.address]
  710. site_data["privatekey"] = privatekey
  711. self.site.updateWebsocket(set_privatekey=bool(privatekey))
  712. self.user.save()
  713. return "ok"
  714. @flag.admin
  715. @flag.no_multiuser
  716. def actionSiteSetAutodownloadoptional(self, to, owned):
  717. self.site.settings["autodownloadoptional"] = bool(owned)
  718. self.site.worker_manager.removeSolvedFileTasks()
  719. @flag.no_multiuser
  720. @flag.admin
  721. def actionDbReload(self, to):
  722. self.site.storage.closeDb()
  723. self.site.storage.getDb()
  724. return self.response(to, "ok")
  725. @flag.no_multiuser
  726. @flag.admin
  727. def actionDbRebuild(self, to):
  728. try:
  729. self.site.storage.rebuildDb()
  730. except Exception as err:
  731. return self.response(to, {"error": str(err)})
  732. return self.response(to, "ok")