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