Add files via upload

We recovered Zeronet Enhanced/ZNE from here web.archive.org/https://github.com/zeronet-enhanced/ZeroNet/
This commit is contained in:
wupg98 2023-09-04 05:41:23 +02:00 committed by GitHub
parent 1b1a72bc4a
commit 9aa53f848f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 15884 additions and 0 deletions

View File

@ -0,0 +1,193 @@
import os
import re
import gevent
from Plugin import PluginManager
from Config import config
from Debug import Debug
# Keep archive open for faster reponse times for large sites
archive_cache = {}
def closeArchive(archive_path):
if archive_path in archive_cache:
del archive_cache[archive_path]
def openArchive(archive_path, file_obj=None):
if archive_path not in archive_cache:
if archive_path.endswith("tar.gz"):
import tarfile
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz")
else:
import zipfile
archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path)
gevent.spawn_later(5, lambda: closeArchive(archive_path)) # Close after 5 sec
archive = archive_cache[archive_path]
return archive
def openArchiveFile(archive_path, path_within, file_obj=None):
archive = openArchive(archive_path, file_obj=file_obj)
if archive_path.endswith(".zip"):
return archive.open(path_within)
else:
return archive.extractfile(path_within)
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionSiteMedia(self, path, **kwargs):
if ".zip/" in path or ".tar.gz/" in path:
file_obj = None
path_parts = self.parsePath(path)
file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"])
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path)
archive_path, path_within = match.groups()
if archive_path not in archive_cache:
site = self.server.site_manager.get(path_parts["address"])
if not site:
return self.actionSiteAddPrompt(path)
archive_inner_path = site.storage.getInnerPath(archive_path)
if not os.path.isfile(archive_path):
# Wait until file downloads
result = site.needFile(archive_inner_path, priority=10)
# Send virutal file path download finished event to remove loading screen
site.updateWebsocket(file_done=archive_inner_path)
if not result:
return self.error404(archive_inner_path)
file_obj = site.storage.openBigfile(archive_inner_path)
if file_obj == False:
file_obj = None
header_allow_ajax = False
if self.get.get("ajax_key"):
requester_site = self.server.site_manager.get(path_parts["request_address"])
if self.get["ajax_key"] == requester_site.settings["ajax_key"]:
header_allow_ajax = True
else:
return self.error403("Invalid ajax_key")
try:
file = openArchiveFile(archive_path, path_within, file_obj=file_obj)
content_type = self.getContentType(file_path)
self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False), allow_ajax=header_allow_ajax)
return self.streamFile(file)
except Exception as err:
self.log.debug("Error opening archive file: %s" % Debug.formatException(err))
return self.error404(path)
return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
def streamFile(self, file):
for i in range(100): # Read max 6MB
try:
block = file.read(60 * 1024)
if block:
yield block
else:
raise StopIteration
except StopIteration:
file.close()
break
@PluginManager.registerTo("SiteStorage")
class SiteStoragePlugin(object):
def isFile(self, inner_path):
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
archive_inner_path, path_within = match.groups()
return super(SiteStoragePlugin, self).isFile(archive_inner_path)
else:
return super(SiteStoragePlugin, self).isFile(inner_path)
def openArchive(self, inner_path):
archive_path = self.getPath(inner_path)
file_obj = None
if archive_path not in archive_cache:
if not os.path.isfile(archive_path):
result = self.site.needFile(inner_path, priority=10)
self.site.updateWebsocket(file_done=inner_path)
if not result:
raise Exception("Unable to download file")
file_obj = self.site.storage.openBigfile(inner_path)
if file_obj == False:
file_obj = None
try:
archive = openArchive(archive_path, file_obj=file_obj)
except Exception as err:
raise Exception("Unable to download file: %s" % Debug.formatException(err))
return archive
def walk(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
if archive_inner_path.endswith(".zip"):
namelist = [name for name in archive.namelist() if not name.endswith("/")]
else:
namelist = [item.name for item in archive.getmembers() if not item.isdir()]
namelist_relative = []
for name in namelist:
if not name.startswith(path_within):
continue
name_relative = name.replace(path_within, "", 1).rstrip("/")
namelist_relative.append(name_relative)
return namelist_relative
else:
return super(SiteStoragePlugin, self).walk(inner_path, *args, **kwags)
def list(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
if archive_inner_path.endswith(".zip"):
namelist = [name for name in archive.namelist()]
else:
namelist = [item.name for item in archive.getmembers()]
namelist_relative = []
for name in namelist:
if not name.startswith(path_within):
continue
name_relative = name.replace(path_within, "", 1).rstrip("/")
if "/" in name_relative: # File is in sub-directory
continue
namelist_relative.append(name_relative)
return namelist_relative
else:
return super(SiteStoragePlugin, self).list(inner_path, *args, **kwags)
def read(self, inner_path, mode="rb", **kwargs):
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
if archive_inner_path.endswith(".zip"):
return archive.open(path_within).read()
else:
return archive.extractfile(path_within).read()
else:
return super(SiteStoragePlugin, self).read(inner_path, mode, **kwargs)

View File

@ -0,0 +1 @@
from . import FilePackPlugin

View File

@ -0,0 +1,5 @@
{
"name": "FilePack",
"description": "Transparent web access for Zip and Tar.gz files.",
"default": "enabled"
}

View File

@ -0,0 +1,187 @@
import time
import re
from Plugin import PluginManager
from Db.DbQuery import DbQuery
from Debug import Debug
from util import helper
from util.Flag import flag
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def formatSiteInfo(self, site, create_user=True):
site_info = super(UiWebsocketPlugin, self).formatSiteInfo(site, create_user=create_user)
feed_following = self.user.sites.get(site.address, {}).get("follow", None)
if feed_following == None:
site_info["feed_follow_num"] = None
else:
site_info["feed_follow_num"] = len(feed_following)
return site_info
def actionFeedFollow(self, to, feeds):
self.user.setFeedFollow(self.site.address, feeds)
self.user.save()
self.response(to, "ok")
def actionFeedListFollow(self, to):
feeds = self.user.sites.get(self.site.address, {}).get("follow", {})
self.response(to, feeds)
@flag.admin
def actionFeedQuery(self, to, limit=10, day_limit=3):
from Site import SiteManager
rows = []
stats = []
total_s = time.time()
num_sites = 0
for address, site_data in list(self.user.sites.items()):
feeds = site_data.get("follow")
if not feeds:
continue
if type(feeds) is not dict:
self.log.debug("Invalid feed for site %s" % address)
continue
num_sites += 1
for name, query_set in feeds.items():
site = SiteManager.site_manager.get(address)
if not site or not site.storage.has_db:
continue
s = time.time()
try:
query_raw, params = query_set
query_parts = re.split(r"UNION(?:\s+ALL|)", query_raw)
for i, query_part in enumerate(query_parts):
db_query = DbQuery(query_part)
if day_limit:
where = " WHERE %s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
if "WHERE" in query_part:
query_part = re.sub("WHERE (.*?)(?=$| GROUP BY)", where+" AND (\\1)", query_part)
else:
query_part += where
query_parts[i] = query_part
query = " UNION ".join(query_parts)
if ":params" in query:
query_params = map(helper.sqlquote, params)
query = query.replace(":params", ",".join(query_params))
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit)
except Exception as err: # Log error
self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err)))
stats.append({"site": site.address, "feed_name": name, "error": str(err)})
continue
for row in res:
row = dict(row)
if not isinstance(row["date_added"], (int, float, complex)):
self.log.debug("Invalid date_added from site %s: %r" % (address, row["date_added"]))
continue
if row["date_added"] > 1000000000000: # Formatted as millseconds
row["date_added"] = row["date_added"] / 1000
if "date_added" not in row or row["date_added"] > time.time() + 120:
self.log.debug("Newsfeed item from the future from from site %s" % address)
continue # Feed item is in the future, skip it
row["site"] = address
row["feed_name"] = name
rows.append(row)
stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)})
time.sleep(0.001)
return self.response(to, {"rows": rows, "stats": stats, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3)})
def parseSearch(self, search):
parts = re.split("(site|type):", search)
if len(parts) > 1: # Found filter
search_text = parts[0]
parts = [part.strip() for part in parts]
filters = dict(zip(parts[1::2], parts[2::2]))
else:
search_text = search
filters = {}
return [search_text, filters]
def actionFeedSearch(self, to, search, limit=30, day_limit=30):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "FeedSearch not allowed")
from Site import SiteManager
rows = []
stats = []
num_sites = 0
total_s = time.time()
search_text, filters = self.parseSearch(search)
for address, site in SiteManager.site_manager.list().items():
if not site.storage.has_db:
continue
if "site" in filters:
if filters["site"].lower() not in [site.address, site.content_manager.contents["content.json"].get("title").lower()]:
continue
if site.storage.db: # Database loaded
feeds = site.storage.db.schema.get("feeds")
else:
try:
feeds = site.storage.loadJson("dbschema.json").get("feeds")
except:
continue
if not feeds:
continue
num_sites += 1
for name, query in feeds.items():
s = time.time()
try:
db_query = DbQuery(query)
params = []
# Filters
if search_text:
db_query.wheres.append("(%s LIKE ? OR %s LIKE ?)" % (db_query.fields["body"], db_query.fields["title"]))
search_like = "%" + search_text.replace(" ", "%") + "%"
params.append(search_like)
params.append(search_like)
if filters.get("type") and filters["type"] not in query:
continue
if day_limit:
db_query.wheres.append(
"%s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
)
# Order
db_query.parts["ORDER BY"] = "date_added DESC"
db_query.parts["LIMIT"] = str(limit)
res = site.storage.query(str(db_query), params)
except Exception as err:
self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err)))
stats.append({"site": site.address, "feed_name": name, "error": str(err), "query": query})
continue
for row in res:
row = dict(row)
if not row["date_added"] or row["date_added"] > time.time() + 120:
continue # Feed item is in the future, skip it
row["site"] = address
row["feed_name"] = name
rows.append(row)
stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)})
return self.response(to, {"rows": rows, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3), "stats": stats})
@PluginManager.registerTo("User")
class UserPlugin(object):
# Set queries that user follows
def setFeedFollow(self, address, feeds):
site_data = self.getSiteData(address)
site_data["follow"] = feeds
self.save()
return site_data

View File

@ -0,0 +1 @@
from . import NewsfeedPlugin

View File

@ -0,0 +1,414 @@
import time
import collections
import itertools
import re
import gevent
from util import helper
from Plugin import PluginManager
from Config import config
from Debug import Debug
if "content_db" not in locals().keys(): # To keep between module reloads
content_db = None
@PluginManager.registerTo("ContentDb")
class ContentDbPlugin(object):
def __init__(self, *args, **kwargs):
global content_db
content_db = self
self.filled = {} # Site addresses that already filled from content.json
self.need_filling = False # file_optional table just created, fill data from content.json files
self.time_peer_numbers_updated = 0
self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files)
self.optional_files = collections.defaultdict(dict)
self.optional_files_loaded = False
self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit)
super(ContentDbPlugin, self).__init__(*args, **kwargs)
def getSchema(self):
schema = super(ContentDbPlugin, self).getSchema()
# Need file_optional table
schema["tables"]["file_optional"] = {
"cols": [
["file_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"],
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
["inner_path", "TEXT"],
["hash_id", "INTEGER"],
["size", "INTEGER"],
["peer", "INTEGER DEFAULT 0"],
["uploaded", "INTEGER DEFAULT 0"],
["is_downloaded", "INTEGER DEFAULT 0"],
["is_pinned", "INTEGER DEFAULT 0"],
["time_added", "INTEGER DEFAULT 0"],
["time_downloaded", "INTEGER DEFAULT 0"],
["time_accessed", "INTEGER DEFAULT 0"]
],
"indexes": [
"CREATE UNIQUE INDEX file_optional_key ON file_optional (site_id, inner_path)",
"CREATE INDEX is_downloaded ON file_optional (is_downloaded)"
],
"schema_changed": 11
}
return schema
def initSite(self, site):
super(ContentDbPlugin, self).initSite(site)
if self.need_filling:
self.fillTableFileOptional(site)
def checkTables(self):
changed_tables = super(ContentDbPlugin, self).checkTables()
if "file_optional" in changed_tables:
self.need_filling = True
return changed_tables
# Load optional files ending
def loadFilesOptional(self):
s = time.time()
num = 0
total = 0
total_downloaded = 0
res = content_db.execute("SELECT site_id, inner_path, size, is_downloaded FROM file_optional")
site_sizes = collections.defaultdict(lambda: collections.defaultdict(int))
for row in res:
self.optional_files[row["site_id"]][row["inner_path"][-8:]] = 1
num += 1
# Update site size stats
site_sizes[row["site_id"]]["size_optional"] += row["size"]
if row["is_downloaded"]:
site_sizes[row["site_id"]]["optional_downloaded"] += row["size"]
# Site site size stats to sites.json settings
site_ids_reverse = {val: key for key, val in self.site_ids.items()}
for site_id, stats in site_sizes.items():
site_address = site_ids_reverse.get(site_id)
if not site_address or site_address not in self.sites:
self.log.error("Not found site_id: %s" % site_id)
continue
site = self.sites[site_address]
site.settings["size_optional"] = stats["size_optional"]
site.settings["optional_downloaded"] = stats["optional_downloaded"]
total += stats["size_optional"]
total_downloaded += stats["optional_downloaded"]
self.log.info(
"Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" %
(num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s)
)
if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded:
limit_bytes = self.getOptionalLimitBytes()
limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10%
self.log.info(
"First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" %
(float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new)
)
config.saveValue("optional_limit", limit_new)
config.optional_limit = str(limit_new)
# Predicts if the file is optional
def isOptionalFile(self, site_id, inner_path):
return self.optional_files[site_id].get(inner_path[-8:])
# Fill file_optional table with optional files found in sites
def fillTableFileOptional(self, site):
s = time.time()
site_id = self.site_ids.get(site.address)
if not site_id:
return False
cur = self.getCursor()
res = cur.execute("SELECT * FROM content WHERE size_files_optional > 0 AND site_id = %s" % site_id)
num = 0
for row in res.fetchall():
content = site.content_manager.contents[row["inner_path"]]
try:
num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur)
except Exception as err:
self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err))
cur.close()
# Set my files to pinned
from User import UserManager
user = UserManager.user_manager.get()
if not user:
user = UserManager.user_manager.create()
auth_address = user.getAuthAddress(site.address)
res = self.execute(
"UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path",
{"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address}
)
self.log.debug(
"Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" %
(site.address, time.time() - s, num, res.rowcount)
)
self.filled[site.address] = True
def setContentFilesOptional(self, site, content_inner_path, content, cur=None):
if not cur:
cur = self
num = 0
site_id = self.site_ids[site.address]
content_inner_dir = helper.getDirname(content_inner_path)
for relative_inner_path, file in content.get("files_optional", {}).items():
file_inner_path = content_inner_dir + relative_inner_path
hash_id = int(file["sha512"][0:4], 16)
if hash_id in site.content_manager.hashfield:
is_downloaded = 1
else:
is_downloaded = 0
if site.address + "/" + content_inner_dir in self.my_optional_files:
is_pinned = 1
else:
is_pinned = 0
cur.insertOrUpdate("file_optional", {
"hash_id": hash_id,
"size": int(file["size"])
}, {
"site_id": site_id,
"inner_path": file_inner_path
}, oninsert={
"time_added": int(time.time()),
"time_downloaded": int(time.time()) if is_downloaded else 0,
"is_downloaded": is_downloaded,
"peer": is_downloaded,
"is_pinned": is_pinned
})
self.optional_files[site_id][file_inner_path[-8:]] = 1
num += 1
return num
def setContent(self, site, inner_path, content, size=0):
super(ContentDbPlugin, self).setContent(site, inner_path, content, size=size)
old_content = site.content_manager.contents.get(inner_path, {})
if (not self.need_filling or self.filled.get(site.address)) and ("files_optional" in content or "files_optional" in old_content):
self.setContentFilesOptional(site, inner_path, content)
# Check deleted files
if old_content:
old_files = old_content.get("files_optional", {}).keys()
new_files = content.get("files_optional", {}).keys()
content_inner_dir = helper.getDirname(inner_path)
deleted = [content_inner_dir + key for key in old_files if key not in new_files]
if deleted:
site_id = self.site_ids[site.address]
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": deleted})
def deleteContent(self, site, inner_path):
content = site.content_manager.contents.get(inner_path)
if content and "files_optional" in content:
site_id = self.site_ids[site.address]
content_inner_dir = helper.getDirname(inner_path)
optional_inner_paths = [
content_inner_dir + relative_inner_path
for relative_inner_path in content.get("files_optional", {}).keys()
]
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": optional_inner_paths})
super(ContentDbPlugin, self).deleteContent(site, inner_path)
def updatePeerNumbers(self):
s = time.time()
num_file = 0
num_updated = 0
num_site = 0
for site in list(self.sites.values()):
if not site.content_manager.has_optional_files:
continue
if not site.isServing():
continue
has_updated_hashfield = next((
peer
for peer in site.peers.values()
if peer.has_hashfield and peer.hashfield.time_changed > self.time_peer_numbers_updated
), None)
if not has_updated_hashfield and site.content_manager.hashfield.time_changed < self.time_peer_numbers_updated:
continue
hashfield_peers = itertools.chain.from_iterable(
peer.hashfield.storage
for peer in site.peers.values()
if peer.has_hashfield
)
peer_nums = collections.Counter(
itertools.chain(
hashfield_peers,
site.content_manager.hashfield
)
)
site_id = self.site_ids[site.address]
if not site_id:
continue
res = self.execute("SELECT file_id, hash_id, peer FROM file_optional WHERE ?", {"site_id": site_id})
updates = {}
for row in res:
peer_num = peer_nums.get(row["hash_id"], 0)
if peer_num != row["peer"]:
updates[row["file_id"]] = peer_num
for file_id, peer_num in updates.items():
self.execute("UPDATE file_optional SET peer = ? WHERE file_id = ?", (peer_num, file_id))
num_updated += len(updates)
num_file += len(peer_nums)
num_site += 1
self.time_peer_numbers_updated = time.time()
self.log.debug("%s/%s peer number for %s site updated in %.3fs" % (num_updated, num_file, num_site, time.time() - s))
def queryDeletableFiles(self):
# First return the files with atleast 10 seeder and not accessed in last week
query = """
SELECT * FROM file_optional
WHERE peer > 10 AND %s
ORDER BY time_accessed < %s DESC, uploaded / size
""" % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7))
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
self.log.debug("queryDeletableFiles returning less-seeded files")
# Then return files less seeder but still not accessed in last week
query = """
SELECT * FROM file_optional
WHERE peer <= 10 AND %s
ORDER BY peer DESC, time_accessed < %s DESC, uploaded / size
""" % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7))
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
self.log.debug("queryDeletableFiles returning everyting")
# At the end return all files
query = """
SELECT * FROM file_optional
WHERE peer <= 10 AND %s
ORDER BY peer DESC, time_accessed, uploaded / size
""" % self.getOptionalUsedWhere()
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
def getOptionalLimitBytes(self):
if config.optional_limit.endswith("%"):
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
limit_bytes = helper.getFreeSpace() * (limit_percent / 100)
else:
limit_bytes = float(re.sub("[^0-9.]", "", config.optional_limit)) * 1024 * 1024 * 1024
return limit_bytes
def getOptionalUsedWhere(self):
maxsize = config.optional_limit_exclude_minsize * 1024 * 1024
query = "is_downloaded = 1 AND is_pinned = 0 AND size < %s" % maxsize
# Don't delete optional files from owned sites
my_site_ids = []
for address, site in self.sites.items():
if site.settings["own"]:
my_site_ids.append(str(self.site_ids[address]))
if my_site_ids:
query += " AND site_id NOT IN (%s)" % ", ".join(my_site_ids)
return query
def getOptionalUsedBytes(self):
size = self.execute("SELECT SUM(size) FROM file_optional WHERE %s" % self.getOptionalUsedWhere()).fetchone()[0]
if not size:
size = 0
return size
def getOptionalNeedDelete(self, size):
if config.optional_limit.endswith("%"):
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
need_delete = size - ((helper.getFreeSpace() + size) * (limit_percent / 100))
else:
need_delete = size - self.getOptionalLimitBytes()
return need_delete
def checkOptionalLimit(self, limit=None):
if not limit:
limit = self.getOptionalLimitBytes()
if limit < 0:
self.log.debug("Invalid limit for optional files: %s" % limit)
return False
size = self.getOptionalUsedBytes()
need_delete = self.getOptionalNeedDelete(size)
self.log.debug(
"Optional size: %.1fMB/%.1fMB, Need delete: %.1fMB" %
(float(size) / 1024 / 1024, float(limit) / 1024 / 1024, float(need_delete) / 1024 / 1024)
)
if need_delete <= 0:
return False
self.updatePeerNumbers()
site_ids_reverse = {val: key for key, val in self.site_ids.items()}
deleted_file_ids = []
for row in self.queryDeletableFiles():
site_address = site_ids_reverse.get(row["site_id"])
site = self.sites.get(site_address)
if not site:
self.log.error("No site found for id: %s" % row["site_id"])
continue
site.log.debug("Deleting %s %.3f MB left" % (row["inner_path"], float(need_delete) / 1024 / 1024))
deleted_file_ids.append(row["file_id"])
try:
site.content_manager.optionalRemoved(row["inner_path"], row["hash_id"], row["size"])
site.storage.delete(row["inner_path"])
need_delete -= row["size"]
except Exception as err:
site.log.error("Error deleting %s: %s" % (row["inner_path"], err))
if need_delete <= 0:
break
cur = self.getCursor()
for file_id in deleted_file_ids:
cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id})
cur.close()
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
def load(self, *args, **kwargs):
back = super(SiteManagerPlugin, self).load(*args, **kwargs)
if self.sites and not content_db.optional_files_loaded and content_db.conn:
content_db.optional_files_loaded = True
content_db.loadFilesOptional()
return back

View File

@ -0,0 +1,253 @@
import time
import re
import collections
import gevent
from util import helper
from Plugin import PluginManager
from . import ContentDbPlugin
# We can only import plugin host clases after the plugins are loaded
@PluginManager.afterLoad
def importPluginnedClasses():
global config
from Config import config
def processAccessLog():
global access_log
if access_log:
content_db = ContentDbPlugin.content_db
if not content_db.conn:
return False
s = time.time()
access_log_prev = access_log
access_log = collections.defaultdict(dict)
now = int(time.time())
num = 0
for site_id in access_log_prev:
content_db.execute(
"UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
{"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())}
)
num += len(access_log_prev[site_id])
content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s))
def processRequestLog():
global request_log
if request_log:
content_db = ContentDbPlugin.content_db
if not content_db.conn:
return False
s = time.time()
request_log_prev = request_log
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
num = 0
for site_id in request_log_prev:
for inner_path, uploaded in request_log_prev[site_id].items():
content_db.execute(
"UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
{"site_id": site_id, "inner_path": inner_path}
)
num += 1
content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s))
if "access_log" not in locals().keys(): # To keep between module reloads
access_log = collections.defaultdict(dict) # {site_id: {inner_path1: 1, inner_path2: 1...}}
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
helper.timer(61, processAccessLog)
helper.timer(60, processRequestLog)
@PluginManager.registerTo("ContentManager")
class ContentManagerPlugin(object):
def __init__(self, *args, **kwargs):
self.cache_is_pinned = {}
super(ContentManagerPlugin, self).__init__(*args, **kwargs)
def optionalDownloaded(self, inner_path, hash_id, size=None, own=False):
if "|" in inner_path: # Big file piece
file_inner_path, file_range = inner_path.split("|")
else:
file_inner_path = inner_path
self.contents.db.executeDelayed(
"UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 0",
{"now": int(time.time()), "site_id": self.contents.db.site_ids[self.site.address], "inner_path": file_inner_path}
)
return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own)
def optionalRemoved(self, inner_path, hash_id, size=None):
res = self.contents.db.execute(
"UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1",
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
)
if res.rowcount > 0:
back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size)
# Re-add to hashfield if we have other file with the same hash_id
if self.isDownloaded(hash_id=hash_id, force_check_db=True):
self.hashfield.appendHashId(hash_id)
else:
back = False
self.cache_is_pinned = {}
return back
def optionalRenamed(self, inner_path_old, inner_path_new):
back = super(ContentManagerPlugin, self).optionalRenamed(inner_path_old, inner_path_new)
self.cache_is_pinned = {}
self.contents.db.execute(
"UPDATE file_optional SET inner_path = :inner_path_new WHERE site_id = :site_id AND inner_path = :inner_path_old",
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path_old": inner_path_old, "inner_path_new": inner_path_new}
)
return back
def isDownloaded(self, inner_path=None, hash_id=None, force_check_db=False):
if hash_id and not force_check_db and hash_id not in self.hashfield:
return False
if inner_path:
res = self.contents.db.execute(
"SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
)
else:
res = self.contents.db.execute(
"SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND hash_id = :hash_id AND is_downloaded = 1 LIMIT 1",
{"site_id": self.contents.db.site_ids[self.site.address], "hash_id": hash_id}
)
row = res.fetchone()
if row and row["is_downloaded"]:
return True
else:
return False
def isPinned(self, inner_path):
if inner_path in self.cache_is_pinned:
self.site.log.debug("Cached is pinned: %s" % inner_path)
return self.cache_is_pinned[inner_path]
res = self.contents.db.execute(
"SELECT is_pinned FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
)
row = res.fetchone()
if row and row[0]:
is_pinned = True
else:
is_pinned = False
self.cache_is_pinned[inner_path] = is_pinned
self.site.log.debug("Cache set is pinned: %s %s" % (inner_path, is_pinned))
return is_pinned
def setPin(self, inner_path, is_pinned):
content_db = self.contents.db
site_id = content_db.site_ids[self.site.address]
content_db.execute("UPDATE file_optional SET is_pinned = %d WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path})
self.cache_is_pinned = {}
def optionalDelete(self, inner_path):
if self.isPinned(inner_path):
self.site.log.debug("Skip deleting pinned optional file: %s" % inner_path)
return False
else:
return super(ContentManagerPlugin, self).optionalDelete(inner_path)
@PluginManager.registerTo("WorkerManager")
class WorkerManagerPlugin(object):
def doneTask(self, task):
super(WorkerManagerPlugin, self).doneTask(task)
if task["optional_hash_id"] and not self.tasks: # Execute delayed queries immedietly after tasks finished
ContentDbPlugin.content_db.processDelayed()
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def parsePath(self, path):
global access_log
path_parts = super(UiRequestPlugin, self).parsePath(path)
if path_parts:
site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"])
if site_id:
if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]):
access_log[site_id][path_parts["inner_path"]] = 1
return path_parts
@PluginManager.registerTo("FileRequest")
class FileRequestPlugin(object):
def actionGetFile(self, params):
stats = super(FileRequestPlugin, self).actionGetFile(params)
self.recordFileRequest(params["site"], params["inner_path"], stats)
return stats
def actionStreamFile(self, params):
stats = super(FileRequestPlugin, self).actionStreamFile(params)
self.recordFileRequest(params["site"], params["inner_path"], stats)
return stats
def recordFileRequest(self, site_address, inner_path, stats):
if not stats:
# Only track the last request of files
return False
site_id = ContentDbPlugin.content_db.site_ids[site_address]
if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path):
request_log[site_id][inner_path] += stats["bytes_sent"]
@PluginManager.registerTo("Site")
class SitePlugin(object):
def isDownloadable(self, inner_path):
is_downloadable = super(SitePlugin, self).isDownloadable(inner_path)
if is_downloadable:
return is_downloadable
for path in self.settings.get("optional_help", {}).keys():
if inner_path.startswith(path):
return True
return False
def fileForgot(self, inner_path):
if "|" in inner_path and self.content_manager.isPinned(re.sub(r"\|.*", "", inner_path)):
self.log.debug("File %s is pinned, no fileForgot" % inner_path)
return False
else:
return super(SitePlugin, self).fileForgot(inner_path)
def fileDone(self, inner_path):
if "|" in inner_path and self.bad_files.get(inner_path, 0) > 5: # Idle optional file done
inner_path_file = re.sub(r"\|.*", "", inner_path)
num_changed = 0
for key, val in self.bad_files.items():
if key.startswith(inner_path_file) and val > 1:
self.bad_files[key] = 1
num_changed += 1
self.log.debug("Idle optional file piece done, changed retry number of %s pieces." % num_changed)
if num_changed:
gevent.spawn(self.retryBadFiles)
return super(SitePlugin, self).fileDone(inner_path)
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("OptionalManager plugin")
group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)
return super(ConfigPlugin, self).createArguments()

View File

@ -0,0 +1,158 @@
import copy
import pytest
@pytest.mark.usefixtures("resetSettings")
class TestOptionalManager:
def testDbFill(self, site):
contents = site.content_manager.contents
assert len(site.content_manager.hashfield) > 0
assert contents.db.execute("SELECT COUNT(*) FROM file_optional WHERE is_downloaded = 1").fetchone()[0] == len(site.content_manager.hashfield)
def testSetContent(self, site):
contents = site.content_manager.contents
# Add new file
new_content = copy.deepcopy(contents["content.json"])
new_content["files_optional"]["testfile"] = {
"size": 1234,
"sha512": "aaaabbbbcccc"
}
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
contents["content.json"] = new_content
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] > num_optional_files_before
# Remove file
new_content = copy.deepcopy(contents["content.json"])
del new_content["files_optional"]["testfile"]
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
contents["content.json"] = new_content
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
def testDeleteContent(self, site):
contents = site.content_manager.contents
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
del contents["content.json"]
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
def testVerifyFiles(self, site):
contents = site.content_manager.contents
# Add new file
new_content = copy.deepcopy(contents["content.json"])
new_content["files_optional"]["testfile"] = {
"size": 1234,
"sha512": "aaaabbbbcccc"
}
contents["content.json"] = new_content
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
assert not file_row["is_downloaded"]
# Write file from outside of ZeroNet
site.storage.open("testfile", "wb").write(b"A" * 1234) # For quick check hash does not matter only file size
hashfield_len_before = len(site.content_manager.hashfield)
site.storage.verifyFiles(quick_check=True)
assert len(site.content_manager.hashfield) == hashfield_len_before + 1
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
assert file_row["is_downloaded"]
# Delete file outside of ZeroNet
site.storage.delete("testfile")
site.storage.verifyFiles(quick_check=True)
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
assert not file_row["is_downloaded"]
def testVerifyFilesSameHashId(self, site):
contents = site.content_manager.contents
new_content = copy.deepcopy(contents["content.json"])
# Add two files with same hashid (first 4 character)
new_content["files_optional"]["testfile1"] = {
"size": 1234,
"sha512": "aaaabbbbcccc"
}
new_content["files_optional"]["testfile2"] = {
"size": 2345,
"sha512": "aaaabbbbdddd"
}
contents["content.json"] = new_content
assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") == site.content_manager.hashfield.getHashId("aaaabbbbdddd")
# Write files from outside of ZeroNet (For quick check hash does not matter only file size)
site.storage.open("testfile1", "wb").write(b"A" * 1234)
site.storage.open("testfile2", "wb").write(b"B" * 2345)
site.storage.verifyFiles(quick_check=True)
# Make sure that both is downloaded
assert site.content_manager.isDownloaded("testfile1")
assert site.content_manager.isDownloaded("testfile2")
assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") in site.content_manager.hashfield
# Delete one of the files
site.storage.delete("testfile1")
site.storage.verifyFiles(quick_check=True)
assert not site.content_manager.isDownloaded("testfile1")
assert site.content_manager.isDownloaded("testfile2")
assert site.content_manager.hashfield.getHashId("aaaabbbbdddd") in site.content_manager.hashfield
def testIsPinned(self, site):
assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png")
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
assert len(site.content_manager.cache_is_pinned) == 1
site.content_manager.cache_is_pinned = {}
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
def testBigfilePieceReset(self, site):
site.bad_files = {
"data/fake_bigfile.mp4|0-1024": 10,
"data/fake_bigfile.mp4|1024-2048": 10,
"data/fake_bigfile.mp4|2048-3064": 10
}
site.onFileDone("data/fake_bigfile.mp4|0-1024")
assert site.bad_files["data/fake_bigfile.mp4|1024-2048"] == 1
assert site.bad_files["data/fake_bigfile.mp4|2048-3064"] == 1
def testOptionalDelete(self, site):
contents = site.content_manager.contents
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
site.content_manager.setPin("data/img/zeroid.png", False)
new_content = copy.deepcopy(contents["content.json"])
del new_content["files_optional"]["data/img/zerotalk-upvote.png"]
del new_content["files_optional"]["data/img/zeroid.png"]
assert site.storage.isFile("data/img/zerotalk-upvote.png")
assert site.storage.isFile("data/img/zeroid.png")
site.storage.writeJson("content.json", new_content)
site.content_manager.loadContent("content.json", force=True)
assert not site.storage.isFile("data/img/zeroid.png")
assert site.storage.isFile("data/img/zerotalk-upvote.png")
def testOptionalRename(self, site):
contents = site.content_manager.contents
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
new_content = copy.deepcopy(contents["content.json"])
new_content["files_optional"]["data/img/zerotalk-upvote-new.png"] = new_content["files_optional"]["data/img/zerotalk-upvote.png"]
del new_content["files_optional"]["data/img/zerotalk-upvote.png"]
assert site.storage.isFile("data/img/zerotalk-upvote.png")
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
site.storage.writeJson("content.json", new_content)
site.content_manager.loadContent("content.json", force=True)
assert not site.storage.isFile("data/img/zerotalk-upvote.png")
assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png")
assert site.content_manager.isPinned("data/img/zerotalk-upvote-new.png")
assert site.storage.isFile("data/img/zerotalk-upvote-new.png")

View File

@ -0,0 +1 @@
from src.Test.conftest import *

View File

@ -0,0 +1,5 @@
[pytest]
python_files = Test*.py
addopts = -rsxX -v --durations=6
markers =
webtest: mark a test as a webtest.

View File

@ -0,0 +1,396 @@
import re
import time
import html
import os
import gevent
from Plugin import PluginManager
from Config import config
from util import helper
from util.Flag import flag
from Translate import Translate
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate(plugin_dir + "/languages/")
bigfile_sha512_cache = {}
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def __init__(self, *args, **kwargs):
self.time_peer_numbers_updated = 0
super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
# Add file to content.db and set it as pinned
content_db = self.site.content_manager.contents.db
content_inner_dir = helper.getDirname(inner_path)
content_db.my_optional_files[self.site.address + "/" + content_inner_dir] = time.time()
if len(content_db.my_optional_files) > 50: # Keep only last 50
oldest_key = min(
iter(content_db.my_optional_files.keys()),
key=(lambda key: content_db.my_optional_files[key])
)
del content_db.my_optional_files[oldest_key]
return super(UiWebsocketPlugin, self).actionSiteSign(to, privatekey, inner_path, *args, **kwargs)
def updatePeerNumbers(self):
self.site.updateHashfield()
content_db = self.site.content_manager.contents.db
content_db.updatePeerNumbers()
self.site.updateWebsocket(peernumber_updated=True)
def addBigfileInfo(self, row):
global bigfile_sha512_cache
content_db = self.site.content_manager.contents.db
site = content_db.sites[row["address"]]
if not site.settings.get("has_bigfile"):
return False
file_key = row["address"] + "/" + row["inner_path"]
sha512 = bigfile_sha512_cache.get(file_key)
file_info = None
if not sha512:
file_info = site.content_manager.getFileInfo(row["inner_path"])
if not file_info or not file_info.get("piece_size"):
return False
sha512 = file_info["sha512"]
bigfile_sha512_cache[file_key] = sha512
if sha512 in site.storage.piecefields:
piecefield = site.storage.piecefields[sha512].tobytes()
else:
piecefield = None
if piecefield:
row["pieces"] = len(piecefield)
row["pieces_downloaded"] = piecefield.count(b"\x01")
row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"]
if row["pieces_downloaded"]:
if row["pieces"] == row["pieces_downloaded"]:
row["bytes_downloaded"] = row["size"]
else:
if not file_info:
file_info = site.content_manager.getFileInfo(row["inner_path"])
row["bytes_downloaded"] = row["pieces_downloaded"] * file_info.get("piece_size", 0)
else:
row["bytes_downloaded"] = 0
row["is_downloading"] = bool(next((inner_path for inner_path in site.bad_files if inner_path.startswith(row["inner_path"])), False))
# Add leech / seed stats
row["peer_seed"] = 0
row["peer_leech"] = 0
for peer in site.peers.values():
if not peer.time_piecefields_updated or sha512 not in peer.piecefields:
continue
peer_piecefield = peer.piecefields[sha512].tobytes()
if not peer_piecefield:
continue
if peer_piecefield == b"\x01" * len(peer_piecefield):
row["peer_seed"] += 1
else:
row["peer_leech"] += 1
# Add myself
if piecefield:
if row["pieces_downloaded"] == row["pieces"]:
row["peer_seed"] += 1
else:
row["peer_leech"] += 1
return True
# Optional file functions
def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10, filter="downloaded", filter_inner_path=None):
if not address:
address = self.site.address
# Update peer numbers if necessary
content_db = self.site.content_manager.contents.db
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
# Start in new thread to avoid blocking
self.time_peer_numbers_updated = time.time()
gevent.spawn(self.updatePeerNumbers)
if address == "all" and "ADMIN" not in self.permissions:
return self.response(to, {"error": "Forbidden"})
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]):
return self.response(to, "Invalid order_by")
if type(limit) != int:
return self.response(to, "Invalid limit")
back = []
content_db = self.site.content_manager.contents.db
wheres = {}
wheres_raw = []
if "bigfile" in filter:
wheres["size >"] = 1024 * 1024 * 1
if "downloaded" in filter:
wheres_raw.append("(is_downloaded = 1 OR is_pinned = 1)")
if "pinned" in filter:
wheres["is_pinned"] = 1
if filter_inner_path:
wheres["inner_path__like"] = filter_inner_path
if address == "all":
join = "LEFT JOIN site USING (site_id)"
else:
wheres["site_id"] = content_db.site_ids[address]
join = ""
if wheres_raw:
query_wheres_raw = "AND" + " AND ".join(wheres_raw)
else:
query_wheres_raw = ""
query = "SELECT * FROM file_optional %s WHERE ? %s ORDER BY %s LIMIT %s" % (join, query_wheres_raw, orderby, limit)
for row in content_db.execute(query, wheres):
row = dict(row)
if address != "all":
row["address"] = address
if row["size"] > 1024 * 1024:
has_bigfile_info = self.addBigfileInfo(row)
else:
has_bigfile_info = False
if not has_bigfile_info and "bigfile" in filter:
continue
if not has_bigfile_info:
if row["is_downloaded"]:
row["bytes_downloaded"] = row["size"]
row["downloaded_percent"] = 100
else:
row["bytes_downloaded"] = 0
row["downloaded_percent"] = 0
back.append(row)
self.response(to, back)
def actionOptionalFileInfo(self, to, inner_path):
content_db = self.site.content_manager.contents.db
site_id = content_db.site_ids[self.site.address]
# Update peer numbers if necessary
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
# Start in new thread to avoid blocking
self.time_peer_numbers_updated = time.time()
gevent.spawn(self.updatePeerNumbers)
query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1"
res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path})
row = next(res, None)
if row:
row = dict(row)
if row["size"] > 1024 * 1024:
row["address"] = self.site.address
self.addBigfileInfo(row)
self.response(to, row)
else:
self.response(to, None)
def setPin(self, inner_path, is_pinned, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return {"error": "Forbidden"}
site = self.server.sites[address]
site.content_manager.setPin(inner_path, is_pinned)
return "ok"
@flag.no_multiuser
def actionOptionalFilePin(self, to, inner_path, address=None):
if type(inner_path) is not list:
inner_path = [inner_path]
back = self.setPin(inner_path, 1, address)
num_file = len(inner_path)
if back == "ok":
if num_file == 1:
self.cmd("notification", ["done", _["Pinned %s"] % html.escape(helper.getFilename(inner_path[0])), 5000])
else:
self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000])
self.response(to, back)
@flag.no_multiuser
def actionOptionalFileUnpin(self, to, inner_path, address=None):
if type(inner_path) is not list:
inner_path = [inner_path]
back = self.setPin(inner_path, 0, address)
num_file = len(inner_path)
if back == "ok":
if num_file == 1:
self.cmd("notification", ["done", _["Removed pin from %s"] % html.escape(helper.getFilename(inner_path[0])), 5000])
else:
self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000])
self.response(to, back)
@flag.no_multiuser
def actionOptionalFileDelete(self, to, inner_path, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
content_db = site.content_manager.contents.db
site_id = content_db.site_ids[site.address]
res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path, "is_downloaded": 1})
row = next(res, None)
if not row:
return self.response(to, {"error": "Not found in content.db"})
removed = site.content_manager.optionalRemoved(inner_path, row["hash_id"], row["size"])
# if not removed:
# return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]})
content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path})
try:
site.storage.delete(inner_path)
except Exception as err:
return self.response(to, {"error": "File delete error: %s" % err})
site.updateWebsocket(file_delete=inner_path)
if inner_path in site.content_manager.cache_is_pinned:
site.content_manager.cache_is_pinned = {}
self.response(to, "ok")
# Limit functions
@flag.admin
def actionOptionalLimitStats(self, to):
back = {}
back["limit"] = config.optional_limit
back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes()
back["free"] = helper.getFreeSpace()
self.response(to, back)
@flag.no_multiuser
@flag.admin
def actionOptionalLimitSet(self, to, limit):
config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end
config.saveValue("optional_limit", limit)
self.response(to, "ok")
# Distribute help functions
def actionOptionalHelpList(self, to, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
self.response(to, site.settings.get("optional_help", {}))
@flag.no_multiuser
def actionOptionalHelp(self, to, directory, title, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
content_db = site.content_manager.contents.db
site_id = content_db.site_ids[address]
if "optional_help" not in site.settings:
site.settings["optional_help"] = {}
stats = content_db.execute(
"SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path",
{"site_id": site_id, "inner_path": directory + "%"}
).fetchone()
stats = dict(stats)
if not stats["size"]:
stats["size"] = 0
if not stats["num"]:
stats["num"] = 0
self.cmd("notification", [
"done",
_["You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>"] %
(html.escape(title), html.escape(directory)),
10000
])
site.settings["optional_help"][directory] = title
self.response(to, dict(stats))
@flag.no_multiuser
def actionOptionalHelpRemove(self, to, directory, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
try:
del site.settings["optional_help"][directory]
self.response(to, "ok")
except Exception:
self.response(to, {"error": "Not found"})
def cbOptionalHelpAll(self, to, site, value):
site.settings["autodownloadoptional"] = value
self.response(to, value)
@flag.no_multiuser
def actionOptionalHelpAll(self, to, value, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
if value:
if "ADMIN" in self.site.settings["permissions"]:
self.cbOptionalHelpAll(to, site, True)
else:
site_title = site.content_manager.contents["content.json"].get("title", address)
self.cmd(
"confirm",
[
_["Help distribute all new optional files on site <b>%s</b>"] % html.escape(site_title),
_["Yes, I want to help!"]
],
lambda res: self.cbOptionalHelpAll(to, site, True)
)
else:
site.settings["autodownloadoptional"] = False
self.response(to, False)

View File

@ -0,0 +1,2 @@
from . import OptionalManagerPlugin
from . import UiWebsocketPlugin

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "Archivos %s fijados",
"Removed pin from %s files": "Archivos %s que no estan fijados",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Tu empezaste a ayudar a distribuir <b>%s</b>.<br><small>Directorio: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Ayude a distribuir todos los archivos opcionales en el sitio <b>%s</b>",
"Yes, I want to help!": "¡Si, yo quiero ayudar!"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "Fichiers %s épinglés",
"Removed pin from %s files": "Fichiers %s ne sont plus épinglés",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Vous avez commencé à aider à distribuer <b>%s</b>.<br><small>Dossier : %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Aider à distribuer tous les fichiers optionnels du site <b>%s</b>",
"Yes, I want to help!": "Oui, je veux aider !"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "%s fájl rögzítve",
"Removed pin from %s files": "%s fájl rögzítés eltávolítva",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Új segítség a terjesztésben: <b>%s</b>.<br><small>Könyvtár: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Segítség az összes új opcionális fájl terjesztésében az <b>%s</b> oldalon",
"Yes, I want to help!": "Igen, segíteni akarok!"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "%s 件のファイルを固定",
"Removed pin from %s files": "%s 件のファイルの固定を解除",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "あなたはサイト: <b>%s</b> の配布の援助を開始しました。<br><small>ディレクトリ: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "サイト: <b>%s</b> のすべての新しいオプションファイルの配布を援助しますか?",
"Yes, I want to help!": "はい、やります!"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "Arquivos %s fixados",
"Removed pin from %s files": "Arquivos %s não estão fixados",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Você começou a ajudar a distribuir <b>%s</b>.<br><small>Pasta: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Ajude a distribuir todos os novos arquivos opcionais no site <b>%s</b>",
"Yes, I want to help!": "Sim, eu quero ajudar!"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "已固定 %s 個檔",
"Removed pin from %s files": "已解除固定 %s 個檔",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已經開始幫助分發 <b>%s</b> 。<br><small>目錄:%s</small>",
"Help distribute all new optional files on site <b>%s</b>": "你想要幫助分發 <b>%s</b> 網站的所有檔嗎?",
"Yes, I want to help!": "是,我想要幫助!"
}

View File

@ -0,0 +1,7 @@
{
"Pinned %s files": "已固定 %s 个文件",
"Removed pin from %s files": "已解除固定 %s 个文件",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "您已经开始帮助分发 <b>%s</b> 。<br><small>目录:%s</small>",
"Help distribute all new optional files on site <b>%s</b>": "您想要帮助分发 <b>%s</b> 站点的所有文件吗?",
"Yes, I want to help!": "是,我想要帮助!"
}

View File

@ -0,0 +1,108 @@
import time
import sqlite3
import random
import atexit
import gevent
from Plugin import PluginManager
@PluginManager.registerTo("ContentDb")
class ContentDbPlugin(object):
def __init__(self, *args, **kwargs):
atexit.register(self.saveAllPeers)
super(ContentDbPlugin, self).__init__(*args, **kwargs)
def getSchema(self):
schema = super(ContentDbPlugin, self).getSchema()
schema["tables"]["peer"] = {
"cols": [
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
["address", "TEXT NOT NULL"],
["port", "INTEGER NOT NULL"],
["hashfield", "BLOB"],
["reputation", "INTEGER NOT NULL"],
["time_added", "INTEGER NOT NULL"],
["time_found", "INTEGER NOT NULL"],
["time_response", "INTEGER NOT NULL"],
["connection_error", "INTEGER NOT NULL"]
],
"indexes": [
"CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)"
],
"schema_changed": 3
}
return schema
def loadPeers(self, site):
s = time.time()
site_id = self.site_ids.get(site.address)
res = self.execute("SELECT * FROM peer WHERE site_id = :site_id", {"site_id": site_id})
num = 0
num_hashfield = 0
for row in res:
peer = site.addPeer(str(row["address"]), row["port"])
if not peer: # Already exist
continue
if row["hashfield"]:
peer.hashfield.replaceFromBytes(row["hashfield"])
num_hashfield += 1
peer.time_added = row["time_added"]
peer.time_found = row["time_found"]
peer.time_found = row["time_found"]
peer.time_response = row["time_response"]
peer.connection_error = row["connection_error"]
if row["address"].endswith(".onion"):
# Onion peers less likely working
if peer.reputation > 0:
peer.reputation = peer.reputation / 2
else:
peer.reputation -= 1
num += 1
if num_hashfield:
site.content_manager.has_optional_files = True
site.log.debug("%s peers (%s with hashfield) loaded in %.3fs" % (num, num_hashfield, time.time() - s))
def iteratePeers(self, site):
site_id = self.site_ids.get(site.address)
for key, peer in list(site.peers.items()):
address, port = key.rsplit(":", 1)
if peer.has_hashfield:
hashfield = sqlite3.Binary(peer.hashfield.tobytes())
else:
hashfield = ""
yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found), int(peer.time_response), int(peer.connection_error))
def savePeers(self, site, spawn=False):
if spawn:
# Save peers every hour (+random some secs to not update very site at same time)
site.greenlet_manager.spawnLater(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
if not site.peers:
site.log.debug("Peers not saved: No peers found")
return
s = time.time()
site_id = self.site_ids.get(site.address)
cur = self.getCursor()
try:
cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
cur.executemany(
"INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found, time_response, connection_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.iteratePeers(site)
)
except Exception as err:
site.log.error("Save peer error: %s" % err)
site.log.debug("Peers saved in %.3fs" % (time.time() - s))
def initSite(self, site):
super(ContentDbPlugin, self).initSite(site)
site.greenlet_manager.spawnLater(0.5, self.loadPeers, site)
site.greenlet_manager.spawnLater(60*60, self.savePeers, site, spawn=True)
def saveAllPeers(self):
for site in list(self.sites.values()):
try:
self.savePeers(site)
except Exception as err:
site.log.error("Save peer error: %s" % err)

View File

@ -0,0 +1,2 @@
from . import PeerDbPlugin

View File

@ -0,0 +1,5 @@
{
"name": "PeerDb",
"description": "Save/restore peer list on client restart.",
"default": "enabled"
}

View File

@ -0,0 +1,101 @@
import re
import logging
from Plugin import PluginManager
from Config import config
from Debug import Debug
from util import SafeRe
from util.Flag import flag
class WsLogStreamer(logging.StreamHandler):
def __init__(self, stream_id, ui_websocket, filter):
self.stream_id = stream_id
self.ui_websocket = ui_websocket
if filter:
if not SafeRe.isSafePattern(filter):
raise Exception("Not a safe prex pattern")
self.filter_re = re.compile(".*" + filter)
else:
self.filter_re = None
return super(WsLogStreamer, self).__init__()
def emit(self, record):
if self.ui_websocket.ws.closed:
self.stop()
return
line = self.format(record)
if self.filter_re and not self.filter_re.match(line):
return False
self.ui_websocket.cmd("logLineAdd", {"stream_id": self.stream_id, "lines": [line]})
def stop(self):
logging.getLogger('').removeHandler(self)
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def __init__(self, *args, **kwargs):
self.log_streamers = {}
return super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
@flag.no_multiuser
@flag.admin
def actionConsoleLogRead(self, to, filter=None, read_size=32 * 1024, limit=500):
log_file_path = "%s/debug.log" % config.log_dir
log_file = open(log_file_path, encoding="utf-8")
log_file.seek(0, 2)
end_pos = log_file.tell()
log_file.seek(max(0, end_pos - read_size))
if log_file.tell() != 0:
log_file.readline() # Partial line junk
pos_start = log_file.tell()
lines = []
if filter:
assert SafeRe.isSafePattern(filter)
filter_re = re.compile(".*" + filter)
last_match = False
for line in log_file:
if not line.startswith("[") and last_match: # Multi-line log entry
lines.append(line.replace(" ", "&nbsp;"))
continue
if filter and not filter_re.match(line):
last_match = False
continue
last_match = True
lines.append(line)
num_found = len(lines)
lines = lines[-limit:]
return {"lines": lines, "pos_end": log_file.tell(), "pos_start": pos_start, "num_found": num_found}
def addLogStreamer(self, stream_id, filter=None):
logger = WsLogStreamer(stream_id, self, filter)
logger.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)-8s %(name)s %(message)s'))
logger.setLevel(logging.getLevelName("DEBUG"))
logging.getLogger('').addHandler(logger)
return logger
@flag.no_multiuser
@flag.admin
def actionConsoleLogStream(self, to, filter=None):
stream_id = to
self.log_streamers[stream_id] = self.addLogStreamer(stream_id, filter)
self.response(to, {"stream_id": stream_id})
@flag.no_multiuser
@flag.admin
def actionConsoleLogStreamRemove(self, to, stream_id):
try:
self.log_streamers[stream_id].stop()
del self.log_streamers[stream_id]
return "ok"
except Exception as err:
return {"error": Debug.formatException(err)}

View File

@ -0,0 +1,892 @@
import re
import os
import html
import sys
import math
import time
import json
import io
import urllib
import urllib.parse
import gevent
import util
from Config import config
from Plugin import PluginManager
from Debug import Debug
from Translate import Translate
from util import helper
from util.Flag import flag
from .ZipStream import ZipStream
plugin_dir = os.path.dirname(__file__)
media_dir = plugin_dir + "/media"
loc_cache = {}
if "_" not in locals():
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
# Inject our resources to end of original file streams
def actionUiMedia(self, path):
if path == "/uimedia/all.js" or path == "/uimedia/all.css":
# First yield the original file and header
body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
for part in body_generator:
yield part
# Append our media file to the end
ext = re.match(".*(js|css)$", path).group(1)
plugin_media_file = "%s/all.%s" % (media_dir, ext)
if config.debug:
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(plugin_media_file)
if ext == "js":
yield _.translateData(open(plugin_media_file).read()).encode("utf8")
else:
for part in self.actionFile(plugin_media_file, send_header=False):
yield part
elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files
file_name = re.match(".*/(.*)", path).group(1)
plugin_media_file = "%s_globe/%s" % (media_dir, file_name)
if config.debug and path.endswith("all.js"):
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(plugin_media_file)
for part in self.actionFile(plugin_media_file):
yield part
else:
for part in super(UiRequestPlugin, self).actionUiMedia(path):
yield part
def actionZip(self):
address = self.get["address"]
site = self.server.site_manager.get(address)
if not site:
return self.error404("Site not found")
title = site.content_manager.contents.get("content.json", {}).get("title", "")
filename = "%s-backup-%s.zip" % (title, time.strftime("%Y-%m-%d_%H_%M"))
filename_quoted = urllib.parse.quote(filename)
self.sendHeader(content_type="application/zip", extra_headers={'Content-Disposition': 'attachment; filename="%s"' % filename_quoted})
return self.streamZip(site.storage.getPath("."))
def streamZip(self, dir_path):
zs = ZipStream(dir_path)
while 1:
data = zs.read()
if not data:
break
yield data
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def sidebarRenderPeerStats(self, body, site):
# Peers by status
peers_total = len(site.peers)
peers_reachable = 0
peers_connectable = 0
peers_connected = 0
peers_failed = 0
# Peers by type
peers_by_type = {}
type_proper_names = {
'ipv4': 'IPv4',
'ipv6': 'IPv6',
'onion': 'Onion',
'unknown': 'Unknown'
}
type_defs = {
'local-ipv4': {
'order' : -21,
'color' : 'yellow'
},
'local-ipv6': {
'order' : -20,
'color' : 'orange'
},
'ipv4': {
'order' : -11,
'color' : 'blue'
},
'ipv6': {
'order' : -10,
'color' : 'darkblue'
},
'unknown': {
'order' : 10,
'color' : 'red'
},
}
for peer in list(site.peers.values()):
# Peers by status
if peer.isConnected():
peers_connected += 1
elif peer.isConnectable():
peers_connectable += 1
elif peer.isReachable() and not peer.connection_error:
peers_reachable += 1
elif peer.isReachable() and peer.connection_error:
peers_failed += 1
# Peers by type
peer_type = peer.getIpType()
peer_readable_type = type_proper_names.get(peer_type, peer_type)
if helper.isPrivateIp(peer.ip):
peer_type = 'local-' + peer.getIpType()
peer_readable_type = 'Local ' + peer_readable_type
peers_by_type[peer_type] = peers_by_type.get(peer_type, {
'type': peer_type,
'readable_type': _[peer_readable_type],
'order': type_defs.get(peer_type, {}).get('order', 0),
'color': type_defs.get(peer_type, {}).get('color', 'purple'),
'count': 0
})
peers_by_type[peer_type]['count'] += 1
########################################################################
if peers_total:
percent_connected = float(peers_connected) / peers_total
percent_connectable = float(peers_connectable) / peers_total
percent_reachable = float(peers_reachable) / peers_total
percent_failed = float(peers_failed) / peers_total
percent_other = min(0.0, 1.0 - percent_connected - percent_connectable - percent_reachable - percent_failed)
else:
percent_connected = 0
percent_reachable = 0
percent_connectable = 0
percent_failed = 0
percent_other = 0
peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % (
site.content_manager.contents.get("content.json", {}).get("domain", site.address),
",".join(peer_ips)
)
body.append(_("""
<li>
<label>
{_[Peers]}
<small class="label-right"><a href='{copy_link}' id='link-copypeers' class='link-right'>{_[Copy to clipboard]}</a></small>
</label>
<ul class='graph graph-stacked'>
<li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
<li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Peers that were recently connected succefully]}'></li>
<li style='width: {percent_reachable:.0%}' class='reachable back-yellow' title='{_[Peers available for connection]}'></li>
<li style='width: {percent_failed:.0%}' class='connectable back-red' title='{_[Peers recently failed to connect]}'></li>
<li style='width: {percent_other:.0%}' class='connectable back-black' title='{_[Peers with wrong, unknown or invalid network address]}'></li>
</ul>
<ul class='graph-legend'>
<li class='color-green' title='{_[Connected peers]}'><span>{_[Connected]}:</span><b>{peers_connected}</b></li>
<li class='color-blue' title='{_[Peers that were recently connected succefully]}'><span>{_[Connectable]}:</span><b>{peers_connectable}</b></li>
<li class='color-yellow' title='{_[Peers available for connection]}'><span>{_[Reachable]}:</span><b>{peers_reachable}</b></li>
<li class='color-red' title='{_[Peers recently failed to connect]}'><span>{_[Failed]}:</span><b>{peers_failed}</b></li>
<li class='color-black'><span>{_[Total peers]}:</span><b>{peers_total}</b></li>
</ul>
"""))
########################################################################
peers_by_type = sorted(
peers_by_type.values(),
key=lambda t: t['order'],
)
peers_by_type_graph = ''
peers_by_type_legend = ''
if peers_total:
for t in peers_by_type:
peers_by_type_graph += """
<li style='width: %.2f%%' class='%s back-%s' title='%s'></li>
""" % (float(t['count']) * 100.0 / peers_total, t['type'], t["color"], t['readable_type'])
peers_by_type_legend += """
<li class='color-%s'><span>%s:</span><b>%s</b></li>
""" % (t["color"], t['readable_type'], t['count'])
if peers_by_type_legend != '':
body.append(_("""
<li>
<label>
{_[Peer types]}
</label>
<ul class='graph graph-stacked'>
%s
</ul>
<ul class='graph-legend'>
%s
</ul>
<li>
""" % (peers_by_type_graph, peers_by_type_legend)))
def sidebarRenderTransferStats(self, body, site):
recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
transfer_total = recv + sent
if transfer_total:
percent_recv = recv / transfer_total
percent_sent = sent / transfer_total
else:
percent_recv = 0.5
percent_sent = 0.5
body.append(_("""
<li>
<label>{_[Data transfer]}</label>
<ul class='graph graph-stacked'>
<li style='width: {percent_recv:.0%}' class='received back-yellow' title="{_[Received bytes]}"></li>
<li style='width: {percent_sent:.0%}' class='sent back-green' title="{_[Sent bytes]}"></li>
</ul>
<ul class='graph-legend'>
<li class='color-yellow'><span>{_[Received]}:</span><b>{recv:.2f}MB</b></li>
<li class='color-green'<span>{_[Sent]}:</span><b>{sent:.2f}MB</b></li>
</ul>
</li>
"""))
def sidebarRenderFileStats(self, body, site):
body.append(_("""
<li>
<label>
{_[Files]}
<a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
<small class="label-right">
<a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
</small>
</label>
<ul class='graph graph-stacked'>
"""))
extensions = (
("html", "yellow"),
("css", "orange"),
("js", "purple"),
("Image", "green"),
("json", "darkblue"),
("User data", "blue"),
("Other", "white"),
("Total", "black")
)
# Collect stats
size_filetypes = {}
size_total = 0
contents = site.content_manager.listContents() # Without user files
for inner_path in contents:
content = site.content_manager.contents[inner_path]
if "files" not in content or content["files"] is None:
continue
for file_name, file_details in list(content["files"].items()):
size_total += file_details["size"]
ext = file_name.split(".")[-1]
size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
# Get user file sizes
size_user_content = site.content_manager.contents.execute(
"SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
{"not__inner_path": contents}
).fetchone()["size"]
if not size_user_content:
size_user_content = 0
size_filetypes["User data"] = size_user_content
size_total += size_user_content
# The missing difference is content.json sizes
if "json" in size_filetypes:
size_filetypes["json"] += max(0, site.settings["size"] - size_total)
size_total = size_other = site.settings["size"]
# Bar
for extension, color in extensions:
if extension == "Total":
continue
if extension == "Other":
size = max(0, size_other)
elif extension == "Image":
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
size_other -= size
else:
size = size_filetypes.get(extension, 0)
size_other -= size
if size_total == 0:
percent = 0
else:
percent = 100 * (float(size) / size_total)
percent = math.floor(percent * 100) / 100 # Floor to 2 digits
body.append(
"""<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
(percent, _[extension], color, _[extension])
)
# Legend
body.append("</ul><ul class='graph-legend'>")
for extension, color in extensions:
if extension == "Other":
size = max(0, size_other)
elif extension == "Image":
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
elif extension == "Total":
size = size_total
else:
size = size_filetypes.get(extension, 0)
if extension == "js":
title = "javascript"
else:
title = extension
if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
size_formatted = "%.0fMB" % (size / 1024 / 1024)
else:
size_formatted = "%.0fkB" % (size / 1024)
body.append("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
body.append("</ul></li>")
def sidebarRenderSizeLimit(self, body, site):
free_space = helper.getFreeSpace() / 1024 / 1024
size = float(site.settings["size"]) / 1024 / 1024
size_limit = site.getSizeLimit()
percent_used = size / size_limit
body.append(_("""
<li>
<label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,.0f}MB)</small></label>
<input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
<a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
</li>
"""))
def sidebarRenderOptionalFileStats(self, body, site):
size_total = float(site.settings["size_optional"])
size_downloaded = float(site.settings["optional_downloaded"])
if not size_total:
return False
percent_downloaded = size_downloaded / size_total
size_formatted_total = size_total / 1024 / 1024
size_formatted_downloaded = size_downloaded / 1024 / 1024
body.append(_("""
<li>
<label>{_[Optional files]}</label>
<ul class='graph'>
<li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
<li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
</ul>
<ul class='graph-legend'>
<li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
<li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
</ul>
</li>
"""))
return True
def sidebarRenderOptionalFileSettings(self, body, site):
if self.site.settings.get("autodownloadoptional"):
checked = "checked='checked'"
else:
checked = ""
body.append(_("""
<li>
<label>{_[Help distribute added optional files]}</label>
<input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
"""))
if hasattr(config, "autodownload_bigfile_size_limit"):
autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
body.append(_("""
<div class='settings-autodownloadoptional'>
<label>{_[Auto download big file size limit]}</label>
<input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
<a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
<a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
</div>
"""))
body.append("</li>")
def sidebarRenderBadFiles(self, body, site):
body.append(_("""
<li>
<label>{_[Needs to be updated]}:</label>
<ul class='filelist'>
"""))
i = 0
for bad_file, tries in site.bad_files.items():
i += 1
body.append(_("""<li class='color-red' title="{bad_file_path} ({tries})">{bad_filename}</li>""", {
"bad_file_path": bad_file,
"bad_filename": helper.getFilename(bad_file),
"tries": _.pluralize(tries, "{} try", "{} tries")
}))
if i > 30:
break
if len(site.bad_files) > 30:
num_bad_files = len(site.bad_files) - 30
body.append(_("""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
body.append("""
</ul>
</li>
""")
def sidebarRenderDbOptions(self, body, site):
if site.storage.db:
inner_path = site.storage.getInnerPath(site.storage.db.db_path)
size = float(site.storage.getSize(inner_path)) / 1024
feeds = len(site.storage.db.schema.get("feeds", {}))
else:
inner_path = _["No database found"]
size = 0.0
feeds = 0
body.append(_("""
<li>
<label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
<div class='flex'>
<input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
<a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
<a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
</div>
</li>
""", nested=True))
def sidebarRenderIdentity(self, body, site):
auth_address = self.user.getAuthAddress(self.site.address, create=False)
rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
if rules and rules.get("max_size"):
quota = rules["max_size"] / 1024
try:
content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())])
except:
used = 0
used = used / 1024
else:
quota = used = 0
body.append(_("""
<li>
<label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
<div class='flex'>
<span class='input text disabled'>{auth_address}</span>
<a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
</div>
</li>
"""))
def sidebarRenderControls(self, body, site):
auth_address = self.user.getAuthAddress(self.site.address, create=False)
if self.site.settings["serving"]:
class_pause = ""
class_resume = "hidden"
else:
class_pause = "hidden"
class_resume = ""
body.append(_("""
<li>
<label>{_[Site control]}</label>
<a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
<a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
<a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
<a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
</li>
"""))
donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
site_address = self.site.address
body.append(_("""
<li>
<label>{_[Site address]}</label><br>
<div class='flex'>
<span class='input text disabled'>{site_address}</span>
"""))
if donate_key == False or donate_key == "":
pass
elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
body.append(_("""
</div>
</li>
<li>
<label>{_[Donate]}</label><br>
<div class='flex'>
{donate_key}
"""))
else:
body.append(_("""
<a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
"""))
body.append(_("""
</div>
</li>
"""))
def sidebarRenderOwnedCheckbox(self, body, site):
if self.site.settings["own"]:
checked = "checked='checked'"
else:
checked = ""
body.append(_("""
<h2 class='owned-title'>{_[This is my site]}</h2>
<input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
"""))
def sidebarRenderOwnSettings(self, body, site):
title = site.content_manager.contents.get("content.json", {}).get("title", "")
description = site.content_manager.contents.get("content.json", {}).get("description", "")
body.append(_("""
<li>
<label for='settings-title'>{_[Site title]}</label>
<input type='text' class='text' value="{title}" id='settings-title'/>
</li>
<li>
<label for='settings-description'>{_[Site description]}</label>
<input type='text' class='text' value="{description}" id='settings-description'/>
</li>
<li>
<a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
</li>
"""))
def sidebarRenderContents(self, body, site):
has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
if has_privatekey:
tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
else:
tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
body.append(_("""
<li>
<label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
""".replace("{tag_privatekey}", tag_privatekey)))
# Choose content you want to sign
body.append(_("""
<div class='flex'>
<input type='text' class='text' value="content.json" id='input-contents'/>
<a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
<a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
</div>
"""))
contents = ["content.json"]
contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
body.append(_("<div class='contents'>{_[Choose]}: "))
for content in contents:
body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
body.append("</div>")
body.append("</li>")
@flag.admin
def actionSidebarGetHtmlTag(self, to):
site = self.site
body = []
body.append("<div>")
body.append("<a href='#Close' class='close'>&times;</a>")
body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
body.append("<div class='globe loading'></div>")
body.append("<ul class='fields'>")
self.sidebarRenderPeerStats(body, site)
self.sidebarRenderTransferStats(body, site)
self.sidebarRenderFileStats(body, site)
self.sidebarRenderSizeLimit(body, site)
has_optional = self.sidebarRenderOptionalFileStats(body, site)
if has_optional:
self.sidebarRenderOptionalFileSettings(body, site)
self.sidebarRenderDbOptions(body, site)
self.sidebarRenderIdentity(body, site)
self.sidebarRenderControls(body, site)
if site.bad_files:
self.sidebarRenderBadFiles(body, site)
self.sidebarRenderOwnedCheckbox(body, site)
body.append("<div class='settings-owned'>")
self.sidebarRenderOwnSettings(body, site)
self.sidebarRenderContents(body, site)
body.append("</div>")
body.append("</ul>")
body.append("</div>")
body.append("<div class='menu template'>")
body.append("<a href='#'' class='menu-item template'>Template</a>")
body.append("</div>")
self.response(to, "".join(body))
def downloadGeoLiteDb(self, db_path):
import gzip
import shutil
from util import helper
if config.offline:
return False
self.log.info("Downloading GeoLite2 City database...")
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
db_urls = [
"https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
"https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
]
for db_url in db_urls:
downloadl_err = None
try:
# Download
response = helper.httpRequest(db_url)
data_size = response.getheader('content-length')
data_recv = 0
data = io.BytesIO()
while True:
buff = response.read(1024 * 512)
if not buff:
break
data.write(buff)
data_recv += 1024 * 512
if data_size:
progress = int(float(data_recv) / int(data_size) * 100)
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
data.seek(0)
# Unpack
with gzip.GzipFile(fileobj=data) as gzip_file:
shutil.copyfileobj(gzip_file, open(db_path, "wb"))
self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
time.sleep(2) # Wait for notify animation
self.log.info("GeoLite2 City database is ready at: %s" % db_path)
return True
except Exception as err:
download_err = err
self.log.error("Error downloading %s: %s" % (db_url, err))
pass
self.cmd("progress", [
"geolite-info",
_["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
-100
])
def getLoc(self, geodb, ip):
global loc_cache
if ip in loc_cache:
return loc_cache[ip]
else:
try:
loc_data = geodb.get(ip)
except:
loc_data = None
if not loc_data or "location" not in loc_data:
loc_cache[ip] = None
return None
loc = {
"lat": loc_data["location"]["latitude"],
"lon": loc_data["location"]["longitude"],
}
if "city" in loc_data:
loc["city"] = loc_data["city"]["names"]["en"]
if "country" in loc_data:
loc["country"] = loc_data["country"]["names"]["en"]
loc_cache[ip] = loc
return loc
@util.Noparallel()
def getGeoipDb(self):
db_name = 'GeoLite2-City.mmdb'
sys_db_paths = []
if sys.platform == "linux":
sys_db_paths += ['/usr/share/GeoIP/' + db_name]
data_dir_db_path = os.path.join(config.data_dir, db_name)
db_paths = sys_db_paths + [data_dir_db_path]
for path in db_paths:
if os.path.isfile(path) and os.path.getsize(path) > 0:
return path
self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
" ".join(db_paths), data_dir_db_path)
if self.downloadGeoLiteDb(data_dir_db_path):
return data_dir_db_path
return None
def getPeerLocations(self, peers):
import maxminddb
db_path = self.getGeoipDb()
if not db_path:
self.log.debug("Not showing peer locations: no GeoIP database")
return False
geodb = maxminddb.open_database(db_path)
peers = list(peers.values())
# Place bars
peer_locations = []
placed = {} # Already placed bars here
for peer in peers:
# Height of bar
if peer.connection and peer.connection.last_ping_delay:
ping = round(peer.connection.last_ping_delay * 1000)
else:
ping = None
loc = self.getLoc(geodb, peer.ip)
if not loc:
continue
# Create position array
lat, lon = loc["lat"], loc["lon"]
latlon = "%s,%s" % (lat, lon)
if latlon in placed and peer.getIpType() == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part
lat += float(128 - int(peer.ip.split(".")[-2])) / 50
lon += float(128 - int(peer.ip.split(".")[-1])) / 50
latlon = "%s,%s" % (lat, lon)
placed[latlon] = True
peer_location = {}
peer_location.update(loc)
peer_location["lat"] = lat
peer_location["lon"] = lon
peer_location["ping"] = ping
peer_locations.append(peer_location)
# Append myself
for ip in self.site.connection_server.ip_external_list:
my_loc = self.getLoc(geodb, ip)
if my_loc:
my_loc["ping"] = 0
peer_locations.append(my_loc)
return peer_locations
@flag.admin
@flag.async_run
def actionSidebarGetPeers(self, to):
try:
peer_locations = self.getPeerLocations(self.site.peers)
globe_data = []
ping_times = [
peer_location["ping"]
for peer_location in peer_locations
if peer_location["ping"]
]
if ping_times:
ping_avg = sum(ping_times) / float(len(ping_times))
else:
ping_avg = 0
for peer_location in peer_locations:
if peer_location["ping"] == 0: # Me
height = -0.135
elif peer_location["ping"]:
height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
else:
height = -0.03
globe_data += [peer_location["lat"], peer_location["lon"], height]
self.response(to, globe_data)
except Exception as err:
self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
self.response(to, {"error": str(err)})
@flag.admin
@flag.no_multiuser
def actionSiteSetOwned(self, to, owned):
if self.site.address == config.updatesite:
return {"error": "You can't change the ownership of the updater site"}
self.site.settings["own"] = bool(owned)
self.site.updateWebsocket(owned=owned)
return "ok"
@flag.admin
@flag.no_multiuser
def actionSiteRecoverPrivatekey(self, to):
from Crypt import CryptBitcoin
site_data = self.user.sites[self.site.address]
if site_data.get("privatekey"):
return {"error": "This site already has saved privated key"}
address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
if not address_index:
return {"error": "No address_index in content.json"}
privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
if privatekey_address == self.site.address:
site_data["privatekey"] = privatekey
self.user.save()
self.site.updateWebsocket(recover_privatekey=True)
return "ok"
else:
return {"error": "Unable to deliver private key for this site from current user's master_seed"}
@flag.admin
@flag.no_multiuser
def actionUserSetSitePrivatekey(self, to, privatekey):
site_data = self.user.sites[self.site.address]
site_data["privatekey"] = privatekey
self.site.updateWebsocket(set_privatekey=bool(privatekey))
self.user.save()
return "ok"
@flag.admin
@flag.no_multiuser
def actionSiteSetAutodownloadoptional(self, to, owned):
self.site.settings["autodownloadoptional"] = bool(owned)
self.site.worker_manager.removeSolvedFileTasks()
@flag.no_multiuser
@flag.admin
def actionDbReload(self, to):
self.site.storage.closeDb()
self.site.storage.getDb()
return self.response(to, "ok")
@flag.no_multiuser
@flag.admin
def actionDbRebuild(self, to):
try:
self.site.storage.rebuildDb()
except Exception as err:
return self.response(to, {"error": str(err)})
return self.response(to, "ok")

View File

@ -0,0 +1,59 @@
import io
import os
import zipfile
class ZipStream(object):
def __init__(self, dir_path):
self.dir_path = dir_path
self.pos = 0
self.buff_pos = 0
self.zf = zipfile.ZipFile(self, 'w', zipfile.ZIP_DEFLATED, allowZip64=True)
self.buff = io.BytesIO()
self.file_list = self.getFileList()
def getFileList(self):
for root, dirs, files in os.walk(self.dir_path):
for file in files:
file_path = root + "/" + file
relative_path = os.path.join(os.path.relpath(root, self.dir_path), file)
yield file_path, relative_path
self.zf.close()
def read(self, size=60 * 1024):
for file_path, relative_path in self.file_list:
self.zf.write(file_path, relative_path)
if self.buff.tell() >= size:
break
self.buff.seek(0)
back = self.buff.read()
self.buff.truncate(0)
self.buff.seek(0)
self.buff_pos += len(back)
return back
def write(self, data):
self.pos += len(data)
self.buff.write(data)
def tell(self):
return self.pos
def seek(self, pos, whence=0):
if pos >= self.buff_pos:
self.buff.seek(pos - self.buff_pos, whence)
self.pos = pos
def flush(self):
pass
if __name__ == "__main__":
zs = ZipStream(".")
out = open("out.zip", "wb")
while 1:
data = zs.read()
print("Write %s" % len(data))
if not data:
break
out.write(data)
out.close()

View File

@ -0,0 +1,2 @@
from . import SidebarPlugin
from . import ConsolePlugin

View File

@ -0,0 +1,81 @@
{
"Peers": "Klienter",
"Connected": "Forbundet",
"Connectable": "Mulige",
"Connectable peers": "Mulige klienter",
"Data transfer": "Data overførsel",
"Received": "Modtaget",
"Received bytes": "Bytes modtaget",
"Sent": "Sendt",
"Sent bytes": "Bytes sendt",
"Files": "Filer",
"Total": "I alt",
"Image": "Image",
"Other": "Andet",
"User data": "Bruger data",
"Size limit": "Side max størrelse",
"limit used": "brugt",
"free space": "fri",
"Set": "Opdater",
"Optional files": "Valgfri filer",
"Downloaded": "Downloadet",
"Download and help distribute all files": "Download og hjælp med at dele filer",
"Total size": "Størrelse i alt",
"Downloaded files": "Filer downloadet",
"Database": "Database",
"search feeds": "søgninger",
"{feeds} query": "{feeds} søgninger",
"Reload": "Genindlæs",
"Rebuild": "Genopbyg",
"No database found": "Ingen database fundet",
"Identity address": "Autorisations ID",
"Change": "Skift",
"Update": "Opdater",
"Pause": "Pause",
"Resume": "Aktiv",
"Delete": "Slet",
"Are you sure?": "Er du sikker?",
"Site address": "Side addresse",
"Donate": "Doner penge",
"Missing files": "Manglende filer",
"{} try": "{} forsøg",
"{} tries": "{} forsøg",
"+ {num_bad_files} more": "+ {num_bad_files} mere",
"This is my site": "Dette er min side",
"Site title": "Side navn",
"Site description": "Side beskrivelse",
"Save site settings": "Gem side opsætning",
"Content publishing": "Indhold offentliggøres",
"Choose": "Vælg",
"Sign": "Signer",
"Publish": "Offentliggør",
"This function is disabled on this proxy": "Denne funktion er slået fra på denne ZeroNet proxyEz a funkció ki van kapcsolva ezen a proxy-n",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City database kunne ikke downloades: {}!<br>Download venligst databasen manuelt og udpak i data folder:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 City database downloadet!",
"Are you sure?": "Er du sikker?",
"Site storage limit modified!": "Side max størrelse ændret!",
"Database schema reloaded!": "Database definition genindlæst!",
"Database rebuilding....": "Genopbygger database...",
"Database rebuilt!": "Database genopbygget!",
"Site updated!": "Side opdateret!",
"Delete this site": "Slet denne side",
"File write error: ": "Fejl ved skrivning af fil: ",
"Site settings saved!": "Side opsætning gemt!",
"Enter your private key:": "Indtast din private nøgle:",
" Signed!": " Signeret!",
"WebGL not supported": "WebGL er ikke supporteret"
}

View File

@ -0,0 +1,81 @@
{
"Peers": "Peers",
"Connected": "Verbunden",
"Connectable": "Verbindbar",
"Connectable peers": "Verbindbare Peers",
"Data transfer": "Datei Transfer",
"Received": "Empfangen",
"Received bytes": "Empfangene Bytes",
"Sent": "Gesendet",
"Sent bytes": "Gesendete Bytes",
"Files": "Dateien",
"Total": "Gesamt",
"Image": "Bilder",
"Other": "Sonstiges",
"User data": "Nutzer Daten",
"Size limit": "Speicher Limit",
"limit used": "Limit benutzt",
"free space": "freier Speicher",
"Set": "Setzten",
"Optional files": "Optionale Dateien",
"Downloaded": "Heruntergeladen",
"Download and help distribute all files": "Herunterladen und helfen alle Dateien zu verteilen",
"Total size": "Gesamte Größe",
"Downloaded files": "Heruntergeladene Dateien",
"Database": "Datenbank",
"search feeds": "Feeds durchsuchen",
"{feeds} query": "{feeds} Abfrage",
"Reload": "Neu laden",
"Rebuild": "Neu bauen",
"No database found": "Keine Datenbank gefunden",
"Identity address": "Identitäts Adresse",
"Change": "Ändern",
"Update": "Aktualisieren",
"Pause": "Pausieren",
"Resume": "Fortsetzen",
"Delete": "Löschen",
"Are you sure?": "Bist du sicher?",
"Site address": "Seiten Adresse",
"Donate": "Spenden",
"Missing files": "Fehlende Dateien",
"{} try": "{} versuch",
"{} tries": "{} versuche",
"+ {num_bad_files} more": "+ {num_bad_files} mehr",
"This is my site": "Das ist meine Seite",
"Site title": "Seiten Titel",
"Site description": "Seiten Beschreibung",
"Save site settings": "Einstellungen der Seite speichern",
"Content publishing": "Inhaltsveröffentlichung",
"Choose": "Wähle",
"Sign": "Signieren",
"Publish": "Veröffentlichen",
"This function is disabled on this proxy": "Diese Funktion ist auf dieser Proxy deaktiviert",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City Datenbank Download Fehler: {}!<br>Bitte manuell herunterladen und die Datei in das Datei Verzeichnis extrahieren:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Herunterladen der GeoLite2 City Datenbank (einmalig, ~20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 City Datenbank heruntergeladen!",
"Are you sure?": "Bist du sicher?",
"Site storage limit modified!": "Speicher Limit der Seite modifiziert!",
"Database schema reloaded!": "Datebank Schema neu geladen!",
"Database rebuilding....": "Datenbank neu bauen...",
"Database rebuilt!": "Datenbank neu gebaut!",
"Site updated!": "Seite aktualisiert!",
"Delete this site": "Diese Seite löschen",
"File write error: ": "Datei schreib fehler:",
"Site settings saved!": "Seiten Einstellungen gespeichert!",
"Enter your private key:": "Gib deinen privaten Schlüssel ein:",
" Signed!": " Signiert!",
"WebGL not supported": "WebGL nicht unterstützt"
}

View File

@ -0,0 +1,79 @@
{
"Peers": "Pares",
"Connected": "Conectados",
"Connectable": "Conectables",
"Connectable peers": "Pares conectables",
"Data transfer": "Transferencia de datos",
"Received": "Recibidos",
"Received bytes": "Bytes recibidos",
"Sent": "Enviados",
"Sent bytes": "Bytes envidados",
"Files": "Ficheros",
"Total": "Total",
"Image": "Imagen",
"Other": "Otro",
"User data": "Datos del usuario",
"Size limit": "Límite de tamaño",
"limit used": "Límite utilizado",
"free space": "Espacio libre",
"Set": "Establecer",
"Optional files": "Ficheros opcionales",
"Downloaded": "Descargado",
"Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros",
"Total size": "Tamaño total",
"Downloaded files": "Ficheros descargados",
"Database": "Base de datos",
"search feeds": "Fuentes de búsqueda",
"{feeds} query": "{feeds} consulta",
"Reload": "Recargar",
"Rebuild": "Reconstruir",
"No database found": "No se ha encontrado la base de datos",
"Identity address": "Dirección de la identidad",
"Change": "Cambiar",
"Update": "Actualizar",
"Pause": "Pausar",
"Resume": "Reanudar",
"Delete": "Borrar",
"Site address": "Dirección del sitio",
"Donate": "Donar",
"Missing files": "Ficheros perdidos",
"{} try": "{} intento",
"{} tries": "{} intentos",
"+ {num_bad_files} more": "+ {num_bad_files} más",
"This is my site": "Este es mi sitio",
"Site title": "Título del sitio",
"Site description": "Descripción del sitio",
"Save site settings": "Guardar la configuración del sitio",
"Content publishing": "Publicación del contenido",
"Choose": "Elegir",
"Sign": "Firmar",
"Publish": "Publicar",
"This function is disabled on this proxy": "Esta función está deshabilitada en este proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "¡Error de la base de datos GeoLite2 {}!<br>Por favor, descárgalo manualmente y descomprime al directorio de datos<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB...",
"GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!",
"Are you sure?": "¿Estás seguro?",
"Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!",
"Database schema reloaded!": "¡Esquema de la base de datos recargado!",
"Database rebuilding....": "Reconstruyendo la base de datos...",
"Database rebuilt!": "¡Base de datos reconstruida!",
"Site updated!": "¡Sitio actualizado!",
"Delete this site": "Borrar este sitio",
"File write error: ": "Error de escritura de fichero",
"Site settings saved!": "¡Configuración del sitio guardada!",
"Enter your private key:": "Introduce tu clave privada",
" Signed!": " ¡firmado!",
"WebGL not supported": "WebGL no está soportado"
}

View File

@ -0,0 +1,82 @@
{
"Peers": "Pairs",
"Connected": "Connectés",
"Connectable": "Accessibles",
"Connectable peers": "Pairs accessibles",
"Data transfer": "Données transférées",
"Received": "Reçues",
"Received bytes": "Bytes reçus",
"Sent": "Envoyées",
"Sent bytes": "Bytes envoyés",
"Files": "Fichiers",
"Total": "Total",
"Image": "Image",
"Other": "Autre",
"User data": "Utilisateurs",
"Size limit": "Taille maximale",
"limit used": "utlisé",
"free space": "libre",
"Set": "Modifier",
"Optional files": "Fichiers optionnels",
"Downloaded": "Téléchargé",
"Download and help distribute all files": "Télécharger et distribuer tous les fichiers",
"Total size": "Taille totale",
"Downloaded files": "Fichiers téléchargés",
"Database": "Base de données",
"search feeds": "recherche",
"{feeds} query": "{feeds} requête",
"Reload": "Recharger",
"Rebuild": "Reconstruire",
"No database found": "Aucune base de données trouvée",
"Identity address": "Adresse d'identité",
"Change": "Modifier",
"Site control": "Opérations",
"Update": "Mettre à jour",
"Pause": "Suspendre",
"Resume": "Reprendre",
"Delete": "Supprimer",
"Are you sure?": "Êtes-vous certain?",
"Site address": "Adresse du site",
"Donate": "Faire un don",
"Missing files": "Fichiers manquants",
"{} try": "{} essai",
"{} tries": "{} essais",
"+ {num_bad_files} more": "+ {num_bad_files} manquants",
"This is my site": "Ce site m'appartient",
"Site title": "Nom du site",
"Site description": "Description du site",
"Save site settings": "Enregistrer les paramètres",
"Content publishing": "Publication du contenu",
"Choose": "Sélectionner",
"Sign": "Signer",
"Publish": "Publier",
"This function is disabled on this proxy": "Cette fonction est désactivé sur ce proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erreur au téléchargement de la base de données GeoLite2: {}!<br>Téléchargez et décompressez dans le dossier data:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Téléchargement de la base de données GeoLite2 (une seule fois, ~20MB)...",
"GeoLite2 City database downloaded!": "Base de données GeoLite2 téléchargée!",
"Are you sure?": "Êtes-vous certain?",
"Site storage limit modified!": "Taille maximale modifiée!",
"Database schema reloaded!": "Base de données rechargée!",
"Database rebuilding....": "Reconstruction de la base de données...",
"Database rebuilt!": "Base de données reconstruite!",
"Site updated!": "Site mis à jour!",
"Delete this site": "Supprimer ce site",
"File write error: ": "Erreur à l'écriture du fichier: ",
"Site settings saved!": "Paramètres du site enregistrés!",
"Enter your private key:": "Entrez votre clé privée:",
" Signed!": " Signé!",
"WebGL not supported": "WebGL n'est pas supporté"
}

View File

@ -0,0 +1,82 @@
{
"Peers": "Csatlakozási pontok",
"Connected": "Csaltakozva",
"Connectable": "Csatlakozható",
"Connectable peers": "Csatlakozható peer-ek",
"Data transfer": "Adatátvitel",
"Received": "Fogadott",
"Received bytes": "Fogadott byte-ok",
"Sent": "Küldött",
"Sent bytes": "Küldött byte-ok",
"Files": "Fájlok",
"Total": "Összesen",
"Image": "Kép",
"Other": "Egyéb",
"User data": "Felh. adat",
"Size limit": "Méret korlát",
"limit used": "felhasznált",
"free space": "szabad hely",
"Set": "Beállít",
"Optional files": "Opcionális fájlok",
"Downloaded": "Letöltött",
"Download and help distribute all files": "Minden opcionális fájl letöltése",
"Total size": "Teljes méret",
"Downloaded files": "Letöltve",
"Database": "Adatbázis",
"search feeds": "Keresés források",
"{feeds} query": "{feeds} lekérdezés",
"Reload": "Újratöltés",
"Rebuild": "Újraépítés",
"No database found": "Adatbázis nem található",
"Identity address": "Azonosító cím",
"Change": "Módosít",
"Site control": "Oldal műveletek",
"Update": "Frissít",
"Pause": "Szünteltet",
"Resume": "Folytat",
"Delete": "Töröl",
"Are you sure?": "Biztos vagy benne?",
"Site address": "Oldal címe",
"Donate": "Támogatás",
"Missing files": "Hiányzó fájlok",
"{} try": "{} próbálkozás",
"{} tries": "{} próbálkozás",
"+ {num_bad_files} more": "+ még {num_bad_files} darab",
"This is my site": "Ez az én oldalam",
"Site title": "Oldal neve",
"Site description": "Oldal leírása",
"Save site settings": "Oldal beállítások mentése",
"Content publishing": "Tartalom publikálás",
"Choose": "Válassz",
"Sign": "Aláírás",
"Publish": "Publikálás",
"This function is disabled on this proxy": "Ez a funkció ki van kapcsolva ezen a proxy-n",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 város adatbázis letöltési hiba: {}!<br>A térképhez töltsd le és csomagold ki a data könyvtárba:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 város adatbázis letöltve!",
"Are you sure?": "Biztos vagy benne?",
"Site storage limit modified!": "Az oldalt méret korlát módosítva!",
"Database schema reloaded!": "Adatbázis séma újratöltve!",
"Database rebuilding....": "Adatbázis újraépítés...",
"Database rebuilt!": "Adatbázis újraépítve!",
"Site updated!": "Az oldal frissítve!",
"Delete this site": "Az oldal törlése",
"File write error: ": "Fájl írási hiba: ",
"Site settings saved!": "Az oldal beállításai elmentve!",
"Enter your private key:": "Add meg a privát kulcsod:",
" Signed!": " Aláírva!",
"WebGL not supported": "WebGL nem támogatott"
}

View File

@ -0,0 +1,81 @@
{
"Peers": "Peer",
"Connected": "Connessi",
"Connectable": "Collegabili",
"Connectable peers": "Peer collegabili",
"Data transfer": "Trasferimento dati",
"Received": "Ricevuti",
"Received bytes": "Byte ricevuti",
"Sent": "Inviati",
"Sent bytes": "Byte inviati",
"Files": "File",
"Total": "Totale",
"Image": "Imagine",
"Other": "Altro",
"User data": "Dati utente",
"Size limit": "Limite dimensione",
"limit used": "limite usato",
"free space": "spazio libero",
"Set": "Imposta",
"Optional files": "File facoltativi",
"Downloaded": "Scaricati",
"Download and help distribute all files": "Scarica e aiuta a distribuire tutti i file",
"Total size": "Dimensione totale",
"Downloaded files": "File scaricati",
"Database": "Database",
"search feeds": "ricerca di feed",
"{feeds} query": "{feeds} interrogazione",
"Reload": "Ricaricare",
"Rebuild": "Ricostruire",
"No database found": "Nessun database trovato",
"Identity address": "Indirizzo di identità",
"Change": "Cambia",
"Update": "Aggiorna",
"Pause": "Sospendi",
"Resume": "Riprendi",
"Delete": "Cancella",
"Are you sure?": "Sei sicuro?",
"Site address": "Indirizzo sito",
"Donate": "Dona",
"Missing files": "File mancanti",
"{} try": "{} tenta",
"{} tries": "{} prova",
"+ {num_bad_files} more": "+ {num_bad_files} altri",
"This is my site": "Questo è il mio sito",
"Site title": "Titolo sito",
"Site description": "Descrizione sito",
"Save site settings": "Salva impostazioni sito",
"Content publishing": "Pubblicazione contenuto",
"Choose": "Scegli",
"Sign": "Firma",
"Publish": "Pubblica",
"This function is disabled on this proxy": "Questa funzione è disabilitata su questo proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Errore scaricamento database GeoLite2 City: {}!<br>Si prega di scaricarlo manualmente e spacchetarlo nella cartella dir:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Scaricamento database GeoLite2 City (solo una volta, ~20MB)...",
"GeoLite2 City database downloaded!": "Database GeoLite2 City scaricato!",
"Are you sure?": "Sei sicuro?",
"Site storage limit modified!": "Limite di archiviazione del sito modificato!",
"Database schema reloaded!": "Schema database ricaricato!",
"Database rebuilding....": "Ricostruzione database...",
"Database rebuilt!": "Database ricostruito!",
"Site updated!": "Sito aggiornato!",
"Delete this site": "Cancella questo sito",
"File write error: ": "Errore scrittura file:",
"Site settings saved!": "Impostazioni sito salvate!",
"Enter your private key:": "Inserisci la tua chiave privata:",
" Signed!": " Firmato!",
"WebGL not supported": "WebGL non supportato"
}

View File

@ -0,0 +1,104 @@
{
"Copy to clipboard": "クリップボードにコピー",
"Peers": "ピア",
"Connected": "接続済み",
"Connectable": "利用可能",
"Connectable peers": "ピアに接続可能",
"Onion": "Onion",
"Local": "ローカル",
"Data transfer": "データ転送",
"Received": "受信",
"Received bytes": "受信バイト数",
"Sent": "送信",
"Sent bytes": "送信バイト数",
"Files": "ファイル",
"Browse files": "ファイルを見る",
"Save as .zip": "ZIP形式で保存",
"Total": "合計",
"Image": "画像",
"Other": "その他",
"User data": "ユーザーデータ",
"Size limit": "サイズ制限",
"limit used": "使用上限",
"free space": "フリースペース",
"Set": "セット",
"Optional files": "オプション ファイル",
"Downloaded": "ダウンロード済み",
"Help distribute added optional files": "オプションファイルの配布を支援する",
"Auto download big file size limit": "大きなファイルの自動ダウンロードのサイズ制限",
"Download previous files": "以前のファイルのダウンロード",
"Optional files download started": "オプションファイルのダウンロードを開始",
"Optional files downloaded": "オプションファイルのダウンロードが完了しました",
"Download and help distribute all files": "ダウンロードしてすべてのファイルの配布を支援する",
"Total size": "合計サイズ",
"Downloaded files": "ダウンロードされたファイル",
"Database": "データベース",
"search feeds": "フィードを検索する",
"{feeds} query": "{feeds} お問い合わせ",
"Reload": "再読込",
"Rebuild": "再ビルド",
"No database found": "データベースが見つかりません",
"Identity address": "あなたの識別アドレス",
"Change": "編集",
"Site control": "サイト管理",
"Update": "更新",
"Pause": "一時停止",
"Resume": "再開",
"Delete": "削除",
"Are you sure?": "本当によろしいですか?",
"Site address": "サイトアドレス",
"Donate": "寄付する",
"Missing files": "ファイルがありません",
"{} try": "{} 試す",
"{} tries": "{} 試行",
"+ {num_bad_files} more": "+ {num_bad_files} more",
"This is my site": "これは私のサイトです",
"Site title": "サイトタイトル",
"Site description": "サイトの説明",
"Save site settings": "サイトの設定を保存する",
"Open site directory": "サイトのディレクトリを開く",
"Content publishing": "コンテンツを公開する",
"Add saved private key": "秘密鍵の追加と保存",
"Save": "保存",
"Private key saved.": "秘密鍵が保存されています",
"Private key saved for site signing": "サイトに署名するための秘密鍵を保存",
"Forgot": "わすれる",
"Saved private key removed": "保存された秘密鍵を削除しました",
"Choose": "選択",
"Sign": "署名",
"Publish": "公開する",
"Sign and publish": "署名して公開",
"This function is disabled on this proxy": "この機能はこのプロキシで無効になっています",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Cityデータベースのダウンロードエラー: {}!<br>手動でダウンロードして、フォルダに解凍してください。:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Cityデータベースの読み込み (これは一度だけ行われます, ~20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 Cityデータベースがダウンロードされました",
"Are you sure?": "本当によろしいですか?",
"Site storage limit modified!": "サイトの保存容量の制限が変更されました!",
"Database schema reloaded!": "データベーススキーマがリロードされました!",
"Database rebuilding....": "データベースの再構築中....",
"Database rebuilt!": "データベースが再構築されました!",
"Site updated!": "サイトが更新されました!",
"Delete this site": "このサイトを削除する",
"Blacklist": "NG",
"Blacklist this site": "NGリストに入れる",
"Reason": "理由",
"Delete and Blacklist": "削除してNG",
"File write error: ": "ファイル書き込みエラー:",
"Site settings saved!": "サイト設定が保存されました!",
"Enter your private key:": "秘密鍵を入力してください:",
" Signed!": " 署名しました!",
"WebGL not supported": "WebGLはサポートされていません"
}

View File

@ -0,0 +1,82 @@
{
"Peers": "Użytkownicy równorzędni",
"Connected": "Połączony",
"Connectable": "Możliwy do podłączenia",
"Connectable peers": "Połączeni użytkownicy równorzędni",
"Data transfer": "Transfer danych",
"Received": "Odebrane",
"Received bytes": "Odebrany bajty",
"Sent": "Wysłane",
"Sent bytes": "Wysłane bajty",
"Files": "Pliki",
"Total": "Sumarycznie",
"Image": "Obraz",
"Other": "Inne",
"User data": "Dane użytkownika",
"Size limit": "Rozmiar limitu",
"limit used": "zużyty limit",
"free space": "wolna przestrzeń",
"Set": "Ustaw",
"Optional files": "Pliki opcjonalne",
"Downloaded": "Ściągnięte",
"Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki",
"Total size": "Rozmiar sumaryczny",
"Downloaded files": "Ściągnięte pliki",
"Database": "Baza danych",
"search feeds": "przeszukaj zasoby",
"{feeds} query": "{feeds} pytanie",
"Reload": "Odśwież",
"Rebuild": "Odbuduj",
"No database found": "Nie odnaleziono bazy danych",
"Identity address": "Adres identyfikacyjny",
"Change": "Zmień",
"Site control": "Kontrola strony",
"Update": "Zaktualizuj",
"Pause": "Wstrzymaj",
"Resume": "Wznów",
"Delete": "Skasuj",
"Are you sure?": "Jesteś pewien?",
"Site address": "Adres strony",
"Donate": "Wspomóż",
"Missing files": "Brakujące pliki",
"{} try": "{} próba",
"{} tries": "{} próby",
"+ {num_bad_files} more": "+ {num_bad_files} więcej",
"This is my site": "To moja strona",
"Site title": "Tytuł strony",
"Site description": "Opis strony",
"Save site settings": "Zapisz ustawienia strony",
"Content publishing": "Publikowanie treści",
"Choose": "Wybierz",
"Sign": "Podpisz",
"Publish": "Opublikuj",
"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...",
"GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!",
"Are you sure?": "Jesteś pewien?",
"Site storage limit modified!": "Limit pamięci strony zmodyfikowany!",
"Database schema reloaded!": "Schemat bazy danych załadowany ponownie!",
"Database rebuilding....": "Przebudowywanie bazy danych...",
"Database rebuilt!": "Baza danych przebudowana!",
"Site updated!": "Strona zaktualizowana!",
"Delete this site": "Usuń tę stronę",
"File write error: ": "Błąd zapisu pliku: ",
"Site settings saved!": "Ustawienia strony zapisane!",
"Enter your private key:": "Wpisz swój prywatny klucz:",
" Signed!": " Podpisane!",
"WebGL not supported": "WebGL nie jest obsługiwany"
}

View File

@ -0,0 +1,97 @@
{
"Copy to clipboard": "Copiar para área de transferência (clipboard)",
"Peers": "Peers",
"Connected": "Ligados",
"Connectable": "Disponíveis",
"Onion": "Onion",
"Local": "Locais",
"Connectable peers": "Peers disponíveis",
"Data transfer": "Transferência de dados",
"Received": "Recebidos",
"Received bytes": "Bytes recebidos",
"Sent": "Enviados",
"Sent bytes": "Bytes enviados",
"Files": "Arquivos",
"Save as .zip": "Salvar como .zip",
"Total": "Total",
"Image": "Imagem",
"Other": "Outros",
"User data": "Dados do usuário",
"Size limit": "Limite de tamanho",
"limit used": "limite utilizado",
"free space": "espaço livre",
"Set": "Definir",
"Optional files": "Arquivos opcionais",
"Downloaded": "Baixados",
"Download and help distribute all files": "Baixar e ajudar a distribuir todos os arquivos",
"Total size": "Tamanho total",
"Downloaded files": "Arquivos baixados",
"Database": "Banco de dados",
"search feeds": "pesquisar feeds",
"{feeds} query": "consulta de {feeds}",
"Reload": "Recarregar",
"Rebuild": "Reconstruir",
"No database found": "Base de dados não encontrada",
"Identity address": "Endereço de identidade",
"Change": "Alterar",
"Site control": "Controle do site",
"Update": "Atualizar",
"Pause": "Suspender",
"Resume": "Continuar",
"Delete": "Remover",
"Are you sure?": "Tem certeza?",
"Site address": "Endereço do site",
"Donate": "Doar",
"Needs to be updated": "Necessitam ser atualizados",
"{} try": "{} tentativa",
"{} tries": "{} tentativas",
"+ {num_bad_files} more": "+ {num_bad_files} adicionais",
"This is my site": "Este é o meu site",
"Site title": "Título do site",
"Site description": "Descrição do site",
"Save site settings": "Salvar definições do site",
"Open site directory": "Abrir diretório do site",
"Content publishing": "Publicação do conteúdo",
"Choose": "Escolher",
"Sign": "Assinar",
"Publish": "Publicar",
"Sign and publish": "Assinar e publicar",
"add saved private key": "adicionar privatekey (chave privada) para salvar",
"Private key saved for site signing": "Privatekey foi salva para assinar o site",
"Private key saved.": "Privatekey salva.",
"forgot": "esquecer",
"Saved private key removed": "Privatekey salva foi removida",
"This function is disabled on this proxy": "Esta função encontra-se desativada neste proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erro ao baixar a base de dados GeoLite2 City: {}!<br>Por favor baixe manualmente e descompacte os dados para a seguinte pasta:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Baixando a base de dados GeoLite2 City (uma única vez, ~20MB)...",
"GeoLite2 City database downloaded!": "A base de dados GeoLite2 City foi baixada!",
"Are you sure?": "Tem certeza?",
"Site storage limit modified!": "O limite de armazenamento do site foi modificado!",
"Database schema reloaded!": "O esquema da base de dados foi atualizado!",
"Database rebuilding....": "Reconstruindo base de dados...",
"Database rebuilt!": "Base de dados reconstruída!",
"Site updated!": "Site atualizado!",
"Delete this site": "Remover este site",
"Blacklist": "Blacklist",
"Blacklist this site": "Blacklistar este site",
"Reason": "Motivo",
"Delete and Blacklist": "Deletar e blacklistar",
"File write error: ": "Erro de escrita de arquivo: ",
"Site settings saved!": "Definições do site salvas!",
"Enter your private key:": "Digite sua chave privada:",
" Signed!": " Assinado!",
"WebGL not supported": "WebGL não é suportado",
"Save as .zip": "Salvar como .zip"
}

View File

@ -0,0 +1,82 @@
{
"Peers": "Пиры",
"Connected": "Подключенные",
"Connectable": "Доступные",
"Connectable peers": "Пиры доступны для подключения",
"Data transfer": "Передача данных",
"Received": "Получено",
"Received bytes": "Получено байн",
"Sent": "Отправлено",
"Sent bytes": "Отправлено байт",
"Files": "Файлы",
"Total": "Всего",
"Image": "Изображений",
"Other": "Другое",
"User data": "Ваш контент",
"Size limit": "Ограничение по размеру",
"limit used": "Использовано",
"free space": "Доступно",
"Set": "Установить",
"Optional files": "Опциональные файлы",
"Downloaded": "Загружено",
"Download and help distribute all files": "Загрузить опциональные файлы для помощи сайту",
"Total size": "Объём",
"Downloaded files": "Загруженные файлы",
"Database": "База данных",
"search feeds": "поиск подписок",
"{feeds} query": "{feeds} запрос",
"Reload": "Перезагрузить",
"Rebuild": "Перестроить",
"No database found": "База данных не найдена",
"Identity address": "Уникальный адрес",
"Change": "Изменить",
"Site control": "Управление сайтом",
"Update": "Обновить",
"Pause": "Пауза",
"Resume": "Продолжить",
"Delete": "Удалить",
"Are you sure?": "Вы уверены?",
"Site address": "Адрес сайта",
"Donate": "Пожертвовать",
"Missing files": "Отсутствующие файлы",
"{} try": "{} попробовать",
"{} tries": "{} попыток",
"+ {num_bad_files} more": "+ {num_bad_files} ещё",
"This is my site": "Это мой сайт",
"Site title": "Название сайта",
"Site description": "Описание сайта",
"Save site settings": "Сохранить настройки сайта",
"Content publishing": "Публикация контента",
"Choose": "Выбрать",
"Sign": "Подписать",
"Publish": "Опубликовать",
"This function is disabled on this proxy": "Эта функция отключена на этом прокси",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Ошибка загрузки базы городов GeoLite2: {}!<br>Пожалуйста, загрузите её вручную и распакуйте в папку:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Загрузка базы городов GeoLite2 (это делается только 1 раз, ~20MB)...",
"GeoLite2 City database downloaded!": "База GeoLite2 успешно загружена!",
"Are you sure?": "Вы уверены?",
"Site storage limit modified!": "Лимит хранилища для сайта изменен!",
"Database schema reloaded!": "Схема базы данных перезагружена!",
"Database rebuilding....": "Перестройка базы данных...",
"Database rebuilt!": "База данных перестроена!",
"Site updated!": "Сайт обновлён!",
"Delete this site": "Удалить этот сайт",
"File write error: ": "Ошибка записи файла:",
"Site settings saved!": "Настройки сайта сохранены!",
"Enter your private key:": "Введите свой приватный ключ:",
" Signed!": " Подписано!",
"WebGL not supported": "WebGL не поддерживается"
}

View File

@ -0,0 +1,82 @@
{
"Peers": "Eşler",
"Connected": "Bağlı",
"Connectable": "Erişilebilir",
"Connectable peers": "Bağlanılabilir eşler",
"Data transfer": "Veri aktarımı",
"Received": "Alınan",
"Received bytes": "Bayt alındı",
"Sent": "Gönderilen",
"Sent bytes": "Bayt gönderildi",
"Files": "Dosyalar",
"Total": "Toplam",
"Image": "Resim",
"Other": "Diğer",
"User data": "Kullanıcı verisi",
"Size limit": "Boyut sınırı",
"limit used": "kullanılan",
"free space": "boş",
"Set": "Ayarla",
"Optional files": "İsteğe bağlı dosyalar",
"Downloaded": "İndirilen",
"Download and help distribute all files": "Tüm dosyaları indir ve yayılmalarına yardım et",
"Total size": "Toplam boyut",
"Downloaded files": "İndirilen dosyalar",
"Database": "Veritabanı",
"search feeds": "kaynak ara",
"{feeds} query": "{feeds} sorgu",
"Reload": "Yenile",
"Rebuild": "Yapılandır",
"No database found": "Veritabanı yok",
"Identity address": "Kimlik adresi",
"Change": "Değiştir",
"Site control": "Site kontrolü",
"Update": "Güncelle",
"Pause": "Duraklat",
"Resume": "Sürdür",
"Delete": "Sil",
"Are you sure?": "Emin misin?",
"Site address": "Site adresi",
"Donate": "Bağış yap",
"Missing files": "Eksik dosyalar",
"{} try": "{} deneme",
"{} tries": "{} deneme",
"+ {num_bad_files} more": "+ {num_bad_files} tane daha",
"This is my site": "Bu benim sitem",
"Site title": "Site başlığı",
"Site description": "Site açıklaması",
"Save site settings": "Site ayarlarını kaydet",
"Content publishing": "İçerik yayımlanıyor",
"Choose": "Seç",
"Sign": "İmzala",
"Publish": "Yayımla",
"This function is disabled on this proxy": "Bu özellik bu vekilde kullanılamaz",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Şehir veritabanı indirme hatası: {}!<br>Lütfen kendiniz indirip aşağıdaki konuma açınınız:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Şehir veritabanı indiriliyor (sadece bir kere, ~20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 Şehir veritabanı indirildi!",
"Are you sure?": "Emin misiniz?",
"Site storage limit modified!": "Site saklama sınırı değiştirildi!",
"Database schema reloaded!": "Veritabanı şeması yeniden yüklendi!",
"Database rebuilding....": "Veritabanı yeniden inşa ediliyor...",
"Database rebuilt!": "Veritabanı yeniden inşa edildi!",
"Site updated!": "Site güncellendi!",
"Delete this site": "Bu siteyi sil",
"File write error: ": "Dosya yazma hatası: ",
"Site settings saved!": "Site ayarları kaydedildi!",
"Enter your private key:": "Özel anahtarınızı giriniz:",
" Signed!": " İmzala!",
"WebGL not supported": "WebGL desteklenmiyor"
}

View File

@ -0,0 +1,83 @@
{
"Peers": "節點數",
"Connected": "已連線",
"Connectable": "可連線",
"Connectable peers": "可連線節點",
"Data transfer": "數據傳輸",
"Received": "已接收",
"Received bytes": "已接收位元組",
"Sent": "已傳送",
"Sent bytes": "已傳送位元組",
"Files": "檔案",
"Total": "共計",
"Image": "圖片",
"Other": "其他",
"User data": "使用者數據",
"Size limit": "大小限制",
"limit used": "已使用",
"free space": "可用空間",
"Set": "偏好設定",
"Optional files": "可選檔案",
"Downloaded": "已下載",
"Download and help distribute all files": "下載並幫助分發所有檔案",
"Total size": "總大小",
"Downloaded files": "下載的檔案",
"Database": "資料庫",
"search feeds": "搜尋供稿",
"{feeds} query": "{feeds} 查詢 ",
"Reload": "重新整理",
"Rebuild": "重建",
"No database found": "未找到資料庫",
"Identity address": "身分位址",
"Change": "變更",
"Site control": "網站控制",
"Update": "更新",
"Pause": "暫停",
"Resume": "恢復",
"Delete": "刪除",
"Are you sure?": "你確定?",
"Site address": "網站位址",
"Donate": "捐贈",
"Missing files": "缺少的檔案",
"{} try": "{} 嘗試",
"{} tries": "{} 已嘗試",
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
"This is my site": "這是我的網站",
"Site title": "網站標題",
"Site description": "網站描述",
"Save site settings": "存儲網站設定",
"Open site directory": "打開所在資料夾",
"Content publishing": "內容發布",
"Choose": "選擇",
"Sign": "簽署",
"Publish": "發布",
"Sign and publish": "簽名並發布",
"This function is disabled on this proxy": "此代理上禁用此功能",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置資料庫下載錯誤:{}!<br>請手動下載並解壓到數據目錄:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下載 GeoLite2 地理位置資料庫 (僅一次,約 20MB ...",
"GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!",
"Are you sure?": "你確定?",
"Site storage limit modified!": "網站存儲限制已變更!",
"Database schema reloaded!": "資料庫架構重新加載!",
"Database rebuilding....": "資料庫重建中...",
"Database rebuilt!": "資料庫已重建!",
"Site updated!": "網站已更新!",
"Delete this site": "刪除此網站",
"File write error: ": "檔案寫入錯誤:",
"Site settings saved!": "網站設置已保存!",
"Enter your private key:": "輸入您的私鑰:",
" Signed!": " 已簽署!",
"WebGL not supported": "不支援 WebGL"
}

View File

@ -0,0 +1,101 @@
{
"Copy to clipboard": "复制到剪切板",
"Peers": "节点数",
"Connected": "已连接",
"Connectable": "可连接",
"Onion": "洋葱点",
"Local": "局域网",
"Connectable peers": "可连接节点",
"Data transfer": "数据传输",
"Received": "已接收",
"Received bytes": "已接收字节",
"Sent": "已发送",
"Sent bytes": "已发送字节",
"Files": "文件",
"Save as .zip": "打包成zip文件",
"Total": "总计",
"Image": "图像",
"Other": "其他",
"User data": "用户数据",
"Size limit": "大小限制",
"limit used": "限额",
"free space": "剩余空间",
"Set": "设置",
"Optional files": "可选文件",
"Downloaded": "已下载",
"Help distribute added optional files": "帮助分发新的可选文件",
"Auto download big file size limit": "自动下载大文件大小限制",
"Download previous files": "下载之前的文件",
"Optional files download started": "可选文件下载启动",
"Optional files downloaded": "可选文件下载完成",
"Total size": "总大小",
"Downloaded files": "已下载文件",
"Database": "数据库",
"search feeds": "搜索数据源",
"{feeds} query": "{feeds} 请求",
"Reload": "重载",
"Rebuild": "重建",
"No database found": "没有找到数据库",
"Identity address": "身份地址",
"Change": "更改",
"Site control": "站点控制",
"Update": "更新",
"Pause": "暂停",
"Resume": "恢复",
"Delete": "删除",
"Are you sure?": "您确定吗?",
"Site address": "站点地址",
"Donate": "捐赠",
"Needs to be updated": "需要更新",
"{} try": "{} 尝试",
"{} tries": "{} 已尝试",
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
"This is my site": "这是我的站点",
"Site title": "站点标题",
"Site description": "站点描述",
"Save site settings": "保存站点设置",
"Open site directory": "打开所在文件夹",
"Content publishing": "内容发布",
"Add saved private key": "添加并保存私钥",
"Save": "保存",
"Private key saved.": "私钥已保存",
"Private key saved for site signing": "已保存用于站点签名的私钥",
"Forgot": "删除私钥",
"Saved private key removed": "保存的私钥已删除",
"Choose": "选择",
"Sign": "签名",
"Publish": "发布",
"Sign and publish": "签名并发布",
"This function is disabled on this proxy": "此功能在代理上被禁用",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置数据库下载错误:{}!<br>请手动下载并解压在数据目录:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下载 GeoLite2 地理位置数据库 (仅需一次,约 20MB ...",
"GeoLite2 City database downloaded!": "GeoLite2 地理位置数据库已下载!",
"Are you sure?": "您确定吗?",
"Site storage limit modified!": "站点存储限制已更改!",
"Database schema reloaded!": "数据库模式已重新加载!",
"Database rebuilding....": "数据库重建中...",
"Database rebuilt!": "数据库已重建!",
"Site updated!": "站点已更新!",
"Delete this site": "删除此站点",
"Blacklist": "黑名单",
"Blacklist this site": "拉黑此站点",
"Reason": "原因",
"Delete and Blacklist": "删除并拉黑",
"File write error: ": "文件写入错误:",
"Site settings saved!": "站点设置已保存!",
"Enter your private key:": "输入您的私钥:",
" Signed!": " 已签名!",
"WebGL not supported": "不支持 WebGL"
}

View File

@ -0,0 +1,23 @@
class Class
trace: true
log: (args...) ->
return unless @trace
return if typeof console is 'undefined'
args.unshift("[#{@.constructor.name}]")
console.log(args...)
@
logStart: (name, args...) ->
return unless @trace
@logtimers or= {}
@logtimers[name] = +(new Date)
@log "#{name}", args..., "(started)" if args.length > 0
@
logEnd: (name, args...) ->
ms = +(new Date)-@logtimers[name]
@log "#{name}", args..., "(Done in #{ms}ms)"
@
window.Class = Class

View File

@ -0,0 +1,201 @@
class Console extends Class
constructor: (@sidebar) ->
@tag = null
@opened = false
@filter = null
@tab_types = [
{title: "All", filter: ""},
{title: "Info", filter: "INFO"},
{title: "Warning", filter: "WARNING"},
{title: "Error", filter: "ERROR"}
]
@read_size = 32 * 1024
@tab_active = ""
#@filter = @sidebar.wrapper.site_info.address_short
handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket
@sidebar.wrapper.handleMessageWebsocket = (message) =>
if message.cmd == "logLineAdd" and message.params.stream_id == @stream_id
@addLines(message.params.lines)
else
handleMessageWebsocket_original(message)
$(window).on "hashchange", =>
if window.top.location.hash.startsWith("#ZeroNet:Console")
@open()
if window.top.location.hash.startsWith("#ZeroNet:Console")
setTimeout (=> @open()), 10
createHtmltag: ->
if not @container
@container = $("""
<div class="console-container">
<div class="console">
<div class="console-top">
<div class="console-tabs"></div>
<div class="console-text">Loading...</div>
</div>
<div class="console-middle">
<div class="mynode"></div>
<div class="peers">
<div class="peer"><div class="line"></div><a href="#" class="icon">\u25BD</div></div>
</div>
</div>
</div>
</div>
""")
@text = @container.find(".console-text")
@text_elem = @text[0]
@tabs = @container.find(".console-tabs")
@text.on "mousewheel", (e) => # Stop animation on manual scrolling
if e.originalEvent.deltaY < 0
@text.stop()
RateLimit 300, @checkTextIsBottom
@text.is_bottom = true
@container.appendTo(document.body)
@tag = @container.find(".console")
for tab_type in @tab_types
tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter, "data-title": tab_type.title}).text(tab_type.title)
if tab_type.filter == @tab_active
tab.addClass("active")
tab.on("click", @handleTabClick)
if window.top.location.hash.endsWith(tab_type.title)
@log "Triggering click on", tab
tab.trigger("click")
@tabs.append(tab)
@container.on "mousedown touchend touchcancel", (e) =>
if e.target != e.currentTarget
return true
@log "closing"
if $(document.body).hasClass("body-console")
@close()
return true
@loadConsoleText()
checkTextIsBottom: =>
@text.is_bottom = Math.round(@text_elem.scrollTop + @text_elem.clientHeight) >= @text_elem.scrollHeight - 15
toColor: (text, saturation=60, lightness=70) ->
hash = 0
for i in [0..text.length-1]
hash += text.charCodeAt(i)*i
hash = hash % 1777
return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
formatLine: (line) =>
match = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
if not match
return line.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
[line, added, level, module, text] = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
added = "<span style='color: #dfd0fa'>#{added}</span>"
level = "<span style='color: #{@toColor(level, 100)};'>#{level}</span>"
module = "<span style='color: #{@toColor(module, 60)}; font-weight: bold;'>#{module}</span>"
text = text.replace(/(Site:[A-Za-z0-9\.]+)/g, "<span style='color: #AAAAFF'>$1</span>")
text = text.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
#text = text.replace(/( [0-9\.]+(|s|ms))/g, "<span style='color: #FFF;'>$1</span>")
return "#{added} #{level} #{module} #{text}"
addLines: (lines, animate=true) =>
html_lines = []
@logStart "formatting"
for line in lines
html_lines.push @formatLine(line)
@logEnd "formatting"
@logStart "adding"
@text.append(html_lines.join("<br>") + "<br>")
@logEnd "adding"
if @text.is_bottom and animate
@text.stop().animate({scrollTop: @text_elem.scrollHeight - @text_elem.clientHeight + 1}, 600, 'easeInOutCubic')
loadConsoleText: =>
@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) =>
@text.html("")
pos_diff = res["pos_end"] - res["pos_start"]
size_read = Math.round(pos_diff/1024)
size_total = Math.round(res['pos_end']/1024)
@text.append("<br><br>")
@text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)<br>")
@addLines res.lines, false
@text_elem.scrollTop = @text_elem.scrollHeight
if @stream_id
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
@sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) =>
@stream_id = res.stream_id
close: =>
window.top.location.hash = ""
@sidebar.move_lock = "y"
@sidebar.startDrag()
@sidebar.stopDrag()
open: =>
@sidebar.startDrag()
@sidebar.moved("y")
@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
@sidebar.stopDrag()
onOpened: =>
@sidebar.onClosed()
@log "onOpened"
onClosed: =>
$(document.body).removeClass("body-console")
if @stream_id
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
cleanup: =>
if @container
@container.remove()
@container = null
stopDragY: =>
# Animate sidebar and iframe
if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity
# Closed
targety = 0
@opened = false
else
# Opened
targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity
@onOpened()
@opened = true
# Revent sidebar transitions
if @tag
@tag.css("transition", "0.5s ease-out")
@tag.css("transform", "translateY(#{targety}px)").one transitionEnd, =>
@tag.css("transition", "")
if not @opened
@cleanup()
# Revert body transformations
@log "stopDragY", "opened:", @opened, targety
if not @opened
@onClosed()
changeFilter: (filter) =>
@filter = filter
if @filter == ""
@read_size = 32 * 1024
else
@read_size = 5 * 1024 * 1024
@loadConsoleText()
handleTabClick: (e) =>
elem = $(e.currentTarget)
@tab_active = elem.data("filter")
$("a", @tabs).removeClass("active")
elem.addClass("active")
@changeFilter(@tab_active)
window.top.location.hash = "#ZeroNet:Console:" + elem.data("title")
return false
window.Console = Console

View File

@ -0,0 +1,31 @@
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
.console-container .console { background-color: #212121; height: 100vh; transform: translateY(0px); padding-top: 80px; box-sizing: border-box; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
.console-tabs {
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
box-shadow: -30px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
}
.console-tabs a {
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
}
.console-tabs a:hover { color: #FFF }
.console-tabs a.active { background-color: #46223c; color: #FFF }
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
.console .mynode {
border: 0.5px solid #aaa; width: 50px; height: 50px; transform: rotateZ(45deg); margin-top: -25px; margin-left: -25px;
opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE;
}
.console .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; }
.console .peer { left: 0px; top: 0px; position: absolute; }
.console .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; }
.console .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; transition: all 0.3s }
.console .peer .icon:hover:before { opacity: 1; transition: none }
.console .peer .line {
width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px;
transform: rotateZ(334deg); transform-origin: bottom left;
}

View File

@ -0,0 +1,49 @@
class Menu
constructor: (@button) ->
@elem = $(".menu.template").clone().removeClass("template")
@elem.appendTo("body")
@items = []
show: ->
if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it
window.visible_menu.hide()
@hide()
else
button_pos = @button.offset()
left = button_pos.left
@elem.css({"top": button_pos.top+@button.outerHeight(), "left": left})
@button.addClass("menu-active")
@elem.addClass("visible")
if @elem.position().left + @elem.width() + 20 > window.innerWidth
@elem.css("left", window.innerWidth - @elem.width() - 20)
if window.visible_menu then window.visible_menu.hide()
window.visible_menu = @
hide: ->
@elem.removeClass("visible")
@button.removeClass("menu-active")
window.visible_menu = null
addItem: (title, cb) ->
item = $(".menu-item.template", @elem).clone().removeClass("template")
item.html(title)
item.on "click", =>
if not cb(item)
@hide()
return false
item.appendTo(@elem)
@items.push item
return item
log: (args...) ->
console.log "[Menu]", args...
window.Menu = Menu
# Hide menu on outside click
$("body").on "click", (e) ->
if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0]
window.visible_menu.hide()

View File

@ -0,0 +1,19 @@
.menu {
background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; transform: translate(0px, -30px); pointer-events: none;
box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out;
}
.menu.visible { opacity: 1; max-height: 350px; transform: translate(0px, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all }
.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 30px; }
.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee }
.menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; border: none }
.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none }
.menu-item.selected:before {
content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1);
font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px;
}
@media only screen and (max-width: 800px) {
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
}

View File

@ -0,0 +1,9 @@
String::startsWith = (s) -> @[...s.length] is s
String::endsWith = (s) -> s is '' or @[-s.length..] is s
String::capitalize = -> if @.length then @[0].toUpperCase() + @.slice(1) else ""
String::repeat = (count) -> new Array( count + 1 ).join(@)
window.isEmpty = (obj) ->
for key of obj
return false
return true

View File

@ -0,0 +1,14 @@
limits = {}
call_after_interval = {}
window.RateLimit = (interval, fn) ->
if not limits[fn]
call_after_interval[fn] = false
fn() # First call is not delayed
limits[fn] = setTimeout (->
if call_after_interval[fn]
fn()
delete limits[fn]
delete call_after_interval[fn]
), interval
else # Called within iterval, delay the call
call_after_interval[fn] = true

View File

@ -0,0 +1,91 @@
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
window.initScrollable = function () {
var scrollContainer = document.querySelector('.scrollable'),
scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
scrollContent = document.querySelector('.scrollable .content'),
contentPosition = 0,
scrollerBeingDragged = false,
scroller,
topPosition,
scrollerHeight;
function calculateScrollerHeight() {
// *Calculation of how tall scroller should be
var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
if (visibleRatio == 1)
scroller.style.display = "none";
else
scroller.style.display = "block";
return visibleRatio * scrollContainer.offsetHeight;
}
function moveScroller(evt) {
// Move Scroll bar to top offset
var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
scroller.style.top = topPosition + 'px';
}
function startDrag(evt) {
normalizedPosition = evt.pageY;
contentPosition = scrollContentWrapper.scrollTop;
scrollerBeingDragged = true;
window.addEventListener('mousemove', scrollBarScroll);
return false;
}
function stopDrag(evt) {
scrollerBeingDragged = false;
window.removeEventListener('mousemove', scrollBarScroll);
}
function scrollBarScroll(evt) {
if (scrollerBeingDragged === true) {
evt.preventDefault();
var mouseDifferential = evt.pageY - normalizedPosition;
var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
}
}
function updateHeight() {
scrollerHeight = calculateScrollerHeight() - 10;
scroller.style.height = scrollerHeight + 'px';
}
function createScroller() {
// *Creates scroller element and appends to '.scrollable' div
// create scroller element
scroller = document.createElement("div");
scroller.className = 'scroller';
// determine how big scroller should be based on content
scrollerHeight = calculateScrollerHeight() - 10;
if (scrollerHeight / scrollContainer.offsetHeight < 1) {
// *If there is a need to have scroll bar based on content size
scroller.style.height = scrollerHeight + 'px';
// append scroller to scrollContainer div
scrollContainer.appendChild(scroller);
// show scroll path divot
scrollContainer.className += ' showScroll';
// attach related draggable listeners
scroller.addEventListener('mousedown', startDrag);
window.addEventListener('mouseup', stopDrag);
}
}
createScroller();
// *** Listeners ***
scrollContentWrapper.addEventListener('scroll', moveScroller);
return updateHeight;
};

View File

@ -0,0 +1,44 @@
.scrollable {
overflow: hidden;
}
.scrollable.showScroll::after {
position: absolute;
content: '';
top: 5%;
right: 7px;
height: 90%;
width: 3px;
background: rgba(224, 224, 255, .3);
}
.scrollable .content-wrapper {
width: 100%;
height: 100%;
padding-right: 50%;
overflow-y: scroll;
}
.scroller {
margin-top: 5px;
z-index: 5;
cursor: pointer;
position: absolute;
width: 7px;
border-radius: 5px;
background: #3A3A3A;
top: 0px;
left: 395px;
-webkit-transition: top .08s;
-moz-transition: top .08s;
-ms-transition: top .08s;
-o-transition: top .08s;
transition: top .08s;
}
.scroller {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@ -0,0 +1,644 @@
class Sidebar extends Class
constructor: (@wrapper) ->
@tag = null
@container = null
@opened = false
@width = 410
@console = new Console(@)
@fixbutton = $(".fixbutton")
@fixbutton_addx = 0
@fixbutton_addy = 0
@fixbutton_initx = 0
@fixbutton_inity = 15
@fixbutton_targetx = 0
@move_lock = null
@page_width = $(window).width()
@page_height = $(window).height()
@frame = $("#inner-iframe")
@initFixbutton()
@dragStarted = 0
@globe = null
@preload_html = null
@original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
# Start in opened state for debugging
if window.top.location.hash == "#ZeroNet:OpenSidebar"
@startDrag()
@moved("x")
@fixbutton_targetx = @fixbutton_initx - @width
@stopDrag()
initFixbutton: ->
# Detect dragging
@fixbutton.on "mousedown touchstart", (e) =>
if e.button > 0 # Right or middle click
return
e.preventDefault()
# Disable previous listeners
@fixbutton.off "click touchend touchcancel"
# Make sure its not a click
@dragStarted = (+ new Date)
# Fullscreen drag bg to capture mouse events over iframe
$(".drag-bg").remove()
$("<div class='drag-bg'></div>").appendTo(document.body)
$("body").one "mousemove touchmove", (e) =>
mousex = e.pageX
mousey = e.pageY
if not mousex
mousex = e.originalEvent.touches[0].pageX
mousey = e.originalEvent.touches[0].pageY
@fixbutton_addx = @fixbutton.offset().left - mousex
@fixbutton_addy = @fixbutton.offset().top - mousey
@startDrag()
@fixbutton.parent().on "click touchend touchcancel", (e) =>
if (+ new Date) - @dragStarted < 100
window.top.location = @fixbutton.find(".fixbutton-bg").attr("href")
@stopDrag()
@resized()
$(window).on "resize", @resized
resized: =>
@page_width = $(window).width()
@page_height = $(window).height()
@fixbutton_initx = @page_width - 75 # Initial x position
if @opened
@fixbutton.css
left: @fixbutton_initx - @width
else
@fixbutton.css
left: @fixbutton_initx
# Start dragging the fixbutton
startDrag: ->
#@move_lock = "x" # Temporary until internals not finished
@log "startDrag", @fixbutton_initx, @fixbutton_inity
@fixbutton_targetx = @fixbutton_initx # Fallback x position
@fixbutton_targety = @fixbutton_inity # Fallback y position
@fixbutton.addClass("dragging")
# IE position wrap fix
if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
@fixbutton.css("pointer-events", "none")
# Don't go to homepage
@fixbutton.one "click", (e) =>
@stopDrag()
@fixbutton.removeClass("dragging")
moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx)
moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity)
if moved_x > 5 or moved_y > 10
# If moved more than some pixel the button then don't go to homepage
e.preventDefault()
# Animate drag
@fixbutton.parents().on "mousemove touchmove", @animDrag
@fixbutton.parents().on "mousemove touchmove" ,@waitMove
# Stop dragging listener
@fixbutton.parents().one "mouseup touchend touchcancel", (e) =>
e.preventDefault()
@stopDrag()
# Wait for moving the fixbutton
waitMove: (e) =>
document.body.style.perspective = "1000px"
document.body.style.height = "100%"
document.body.style.willChange = "perspective"
document.documentElement.style.height = "100%"
#$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px")
# $("iframe").css("backface-visibility", "hidden")
moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx)
moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety)
if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50
@moved("x")
@fixbutton.stop().animate {"top": @fixbutton_inity}, 1000
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50
@moved("y")
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
moved: (direction) ->
@log "Moved", direction
@move_lock = direction
if direction == "y"
$(document.body).addClass("body-console")
return @console.createHtmltag()
@createHtmltag()
$(document.body).addClass("body-sidebar")
@container.on "mousedown touchend touchcancel", (e) =>
if e.target != e.currentTarget
return true
@log "closing"
if $(document.body).hasClass("body-sidebar")
@close()
return true
$(window).off "resize"
$(window).on "resize", =>
$(document.body).css "height", $(window).height()
@scrollable()
@resized()
# Override setsiteinfo to catch changes
@wrapper.setSiteInfo = (site_info) =>
@setSiteInfo(site_info)
@original_set_site_info.apply(@wrapper, arguments)
# Preload world.jpg
img = new Image();
img.src = "/uimedia/globe/world.jpg";
setSiteInfo: (site_info) ->
RateLimit 1500, =>
@updateHtmlTag()
RateLimit 30000, =>
@displayGlobe()
# Create the sidebar html tag
createHtmltag: ->
@when_loaded = $.Deferred()
if not @container
@container = $("""
<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
</div></div></div></div>
""")
@container.appendTo(document.body)
@tag = @container.find(".sidebar")
@updateHtmlTag()
@scrollable = window.initScrollable()
updateHtmlTag: ->
if @preload_html
@setHtmlTag(@preload_html)
@preload_html = null
else
@wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
setHtmlTag: (res) =>
if @tag.find(".content").children().length == 0 # First update
@log "Creating content"
@container.addClass("loaded")
morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
# @scrollable()
@when_loaded.resolve()
else # Not first update, patch the html to keep unchanged dom elements
morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
return false
else
return true
}
# Save and forget privatekey for site signing
@tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) =>
@wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) =>
@wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) =>
@wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000
return false
@tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) =>
@wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) =>
if not res
return false
@wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) =>
@wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000
return false
# Use requested address for browse files urls
@tag.find("#browse-files").attr("href", document.location.pathname.replace(/(\/.*?(\/|$)).*$/, "/list$1"))
animDrag: (e) =>
mousex = e.pageX
mousey = e.pageY
if not mousex and e.originalEvent.touches
mousex = e.originalEvent.touches[0].pageX
mousey = e.originalEvent.touches[0].pageY
overdrag = @fixbutton_initx - @width - mousex
if overdrag > 0 # Overdragged
overdrag_percent = 1 + overdrag/300
mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
targetx = @fixbutton_initx - mousex - @fixbutton_addx
targety = @fixbutton_inity - mousey - @fixbutton_addy
if @move_lock == "x"
targety = @fixbutton_inity
else if @move_lock == "y"
targetx = @fixbutton_initx
if not @move_lock or @move_lock == "x"
@fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px"
if @tag
@tag[0].style.transform = "translateX(#{0 - targetx}px)"
if not @move_lock or @move_lock == "y"
@fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px"
if @console.tag
@console.tag[0].style.transform = "translateY(#{0 - targety}px)"
#if @move_lock == "x"
# @fixbutton[0].style.left = "#{@fixbutton_targetx} px"
#@fixbutton[0].style.top = "#{@fixbutton_inity}px"
#if @move_lock == "y"
# @fixbutton[0].style.top = "#{@fixbutton_targety} px"
# Check if opened
if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
@fixbutton_targetx = @fixbutton_initx - @width # Make it opened
else
@fixbutton_targetx = @fixbutton_initx
if (not @console.opened and 0 - targety > @page_height/10) or (@console.opened and 0 - targety > @page_height*0.8)
@fixbutton_targety = @page_height - @fixbutton_inity - 50
else
@fixbutton_targety = @fixbutton_inity
# Stop dragging the fixbutton
stopDrag: ->
@fixbutton.parents().off "mousemove touchmove"
@fixbutton.off "mousemove touchmove"
@fixbutton.css("pointer-events", "")
$(".drag-bg").remove()
if not @fixbutton.hasClass("dragging")
return
@fixbutton.removeClass("dragging")
# Move back to initial position
if @fixbutton_targetx != @fixbutton.offset().left or @fixbutton_targety != @fixbutton.offset().top
# Animate fixbutton
if @move_lock == "y"
top = @fixbutton_targety
left = @fixbutton_initx
if @move_lock == "x"
top = @fixbutton_inity
left = @fixbutton_targetx
@fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", =>
# Switch back to auto align
if @fixbutton_targetx == @fixbutton_initx # Closed
@fixbutton.css("left", "auto")
else # Opened
@fixbutton.css("left", left)
$(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
@stopDragX()
@console.stopDragY()
@move_lock = null
stopDragX: ->
# Animate sidebar and iframe
if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y"
# Closed
targetx = 0
@opened = false
else
# Opened
targetx = @width
if @opened
@onOpened()
else
@when_loaded.done =>
@onOpened()
@opened = true
# Revent sidebar transitions
if @tag
@tag.css("transition", "0.4s ease-out")
@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
@tag.css("transition", "")
if not @opened
@container.remove()
@container = null
if @tag
@tag.remove()
@tag = null
# Revert body transformations
@log "stopdrag", "opened:", @opened
if not @opened
@onClosed()
sign: (inner_path, privatekey) ->
@wrapper.displayProgress("sign", "Signing: #{inner_path}...", 0)
@wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
if res == "ok"
@wrapper.displayProgress("sign", "#{inner_path} signed!", 100)
else
@wrapper.displayProgress("sign", "Error signing #{inner_path}", -1)
publish: (inner_path, privatekey) ->
@wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
if res == "ok"
@wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
handleSiteDeleteClick: ->
if @wrapper.site_info.privatekey
question = "Are you sure?<br>This site has a saved private key"
options = ["Forget private key and delete site"]
else
question = "Are you sure?"
options = ["Delete this site", "Blacklist"]
@wrapper.displayConfirm question, options, (confirmed) =>
if confirmed == 1
@tag.find("#button-delete").addClass("loading")
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
document.location = $(".fixbutton-bg").attr("href")
else if confirmed == 2
@wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) =>
@tag.find("#button-delete").addClass("loading")
@wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason]
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
document.location = $(".fixbutton-bg").attr("href")
onOpened: ->
@log "Opened"
@scrollable()
# Re-calculate height when site admin opened or closed
@tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
setTimeout (=>
@scrollable()
), 300
# Site limit button
@tag.find("#button-sitelimit").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
if res == "ok"
@wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
@updateHtmlTag()
return false
# Site autodownload limit button
@tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) =>
if res == "ok"
@wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000
@updateHtmlTag()
return false
# Site start download optional files
@tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, =>
@wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000
@wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000
return false
# Database reload
@tag.find("#button-dbreload").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "dbReload", [], =>
@wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
@updateHtmlTag()
return false
# Database rebuild
@tag.find("#button-dbrebuild").off("click touchend").on "click touchend", =>
@wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
@wrapper.ws.cmd "dbRebuild", [], =>
@wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
@updateHtmlTag()
return false
# Update site
@tag.find("#button-update").off("click touchend").on "click touchend", =>
@tag.find("#button-update").addClass("loading")
@wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, =>
@wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
@tag.find("#button-update").removeClass("loading")
return false
# Pause site
@tag.find("#button-pause").off("click touchend").on "click touchend", =>
@tag.find("#button-pause").addClass("hidden")
@wrapper.ws.cmd "sitePause", @wrapper.site_info.address
return false
# Resume site
@tag.find("#button-resume").off("click touchend").on "click touchend", =>
@tag.find("#button-resume").addClass("hidden")
@wrapper.ws.cmd "siteResume", @wrapper.site_info.address
return false
# Delete site
@tag.find("#button-delete").off("click touchend").on "click touchend", =>
@handleSiteDeleteClick()
return false
# Owned checkbox
@tag.find("#checkbox-owned").off("click touchend").on "click touchend", =>
owned = @tag.find("#checkbox-owned").is(":checked")
@wrapper.ws.cmd "siteSetOwned", [owned], (res_set_owned) =>
@log "Owned", owned
if owned
@wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) =>
if res_recover == "ok"
@wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000)
else
@log "Unable to recover private key: #{res_recover.error}"
# Owned auto download checkbox
@tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
# Change identity button
@tag.find("#button-identity").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "certSelect"
return false
# Save settings
@tag.find("#button-settings").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "fileGet", "content.json", (res) =>
data = JSON.parse(res)
data["title"] = $("#settings-title").val()
data["description"] = $("#settings-description").val()
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
@wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
if res != "ok" # fileWrite failed
@wrapper.notifications.add "file-write", "error", "File write error: #{res}"
else
@wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
if @wrapper.site_info.privatekey
@wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true}
@updateHtmlTag()
return false
# Open site directory
@tag.find("#link-directory").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address]
return false
# Copy site with peers
@tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) =>
copy_text = e.currentTarget.href
handler = (e) =>
e.clipboardData.setData('text/plain', copy_text)
e.preventDefault()
@wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000
document.removeEventListener('copy', handler, true)
document.addEventListener('copy', handler, true)
document.execCommand('copy')
return false
# Sign and publish content.json
$(document).on "click touchend", =>
@tag?.find("#button-sign-publish-menu").removeClass("visible")
@tag?.find(".contents + .flex").removeClass("sign-publish-flex")
@tag.find(".contents-content").off("click touchend").on "click touchend", (e) =>
$("#input-contents").val(e.currentTarget.innerText);
return false;
menu = new Menu(@tag.find("#menu-sign-publish"))
menu.elem.css("margin-top", "-130px") # Open upwards
menu.addItem "Sign", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
if @wrapper.site_info.auth_address in rules.signers
# ZeroID or other ID provider
@sign(inner_path)
else if @wrapper.site_info.privatekey
# Privatekey stored in users.json
@sign(inner_path, "stored")
else
# Ask the user for privatekey
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
@sign(inner_path, privatekey)
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
menu.addItem "Publish", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
@tag.find("#menu-sign-publish").off("click touchend").on "click touchend", =>
if window.visible_menu == menu
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
else
@tag.find(".contents + .flex").addClass "active"
@tag.find(".content-wrapper").prop "scrollTop", 10000
menu.show()
return false
$("body").on "click", =>
if @tag
@tag.find(".contents + .flex").removeClass "active"
@tag.find("#button-sign-publish").off("click touchend").on "click touchend", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
if @wrapper.site_info.auth_address in rules.signers
# ZeroID or other ID provider
@publish(inner_path, null)
else if @wrapper.site_info.privatekey
# Privatekey stored in users.json
@publish(inner_path, "stored")
else
# Ask the user for privatekey
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
@publish(inner_path, privatekey)
return false
# Close
@tag.find(".close").off("click touchend").on "click touchend", (e) =>
@close()
return false
@loadGlobe()
close: ->
@move_lock = "x"
@startDrag()
@stopDrag()
onClosed: ->
$(window).off "resize"
$(window).on "resize", @resized
$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-console")
$(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd
@unloadGlobe()
# We dont need site info anymore
@wrapper.setSiteInfo = @original_set_site_info
loadGlobe: =>
if @tag.find(".globe").hasClass("loading")
setTimeout (=>
if typeof(DAT) == "undefined" # Globe script not loaded, do it first
script_tag = $("<script>")
script_tag.attr("nonce", @wrapper.script_nonce)
script_tag.attr("src", "/uimedia/globe/all.js")
script_tag.on("load", @displayGlobe)
document.head.appendChild(script_tag[0])
else
@displayGlobe()
), 600
displayGlobe: =>
img = new Image();
img.src = "/uimedia/globe/world.jpg";
img.onload = =>
@wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
if @globe
@globe.scene.remove(@globe.points)
@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
@globe.createPoints()
@tag?.find(".globe").removeClass("loading")
else if typeof(DAT) != "undefined"
try
@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
@globe.createPoints()
@globe.animate()
catch e
console.log "WebGL error", e
@tag?.find(".globe").addClass("error").text("WebGL not supported")
@tag?.find(".globe").removeClass("loading")
unloadGlobe: =>
if not @globe
return false
@globe.unload()
@globe = null
wrapper = window.wrapper
setTimeout ( ->
window.sidebar = new Sidebar(wrapper)
), 500
window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'

View File

@ -0,0 +1,169 @@
.menu {
font-family: Roboto, 'Segoe UI', 'Helvetica Neue'; z-index: 999;
}
.drag-bg { width: 100%; height: 100%; position: fixed; }
.fixbutton.dragging { cursor: -webkit-grabbing; }
.fixbutton-bg:active { cursor: -webkit-grabbing; }
.body-sidebar, .body-console { background-color: #666 !important; }
#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left bottom; }
.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
.body-console iframe { transform: rotateX(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
.sidebar .label-right { float: right; margin-right: 7px; margin-top: 1px; float: right; }
.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; }
.sidebar .link-right:hover { border-color: #CCC; }
.sidebar .link-right:active { background-color: #444 }
.sidebar .link-outline { outline: 1px solid #eee6; padding: 2px 13px; border-bottom: none; font-size: 80%; }
/* SIDEBAR */
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; top: 0px; z-index: 2;}
.sidebar { background-color: #212121; position: fixed; backface-visibility: hidden; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 }
.sidebar-container.loaded .content { opacity: 1; transform: none }
.sidebar h1, .sidebar h2 { font-weight: lighter; }
.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none }
.sidebar .button { margin: 0px; display: inline-block; transition: all 0.3s; box-sizing: border-box; max-width: 260px }
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
.sidebar .flex { display: flex }
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
/* FIELDS */
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
.sidebar .fields label {
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
vertical-align: text-bottom; margin-right: 10px; width: 100%
}
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; border-radius: 3px; width: 260px; font-family: Consolas, monospace; }
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
/* Select */
.sidebar .fields select {
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; appearance: none;
padding: 5px; padding-right: 25px; border: 0px; border-radius: 3px; height: 35px; vertical-align: 1px; box-shadow: 0px 1px 2px rgba(0,0,0,0.5);
}
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; transform: rotateZ(90deg); height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
/* Checkbox */
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; margin-left: -59px; }
.sidebar .fields .checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
/* Fake input */
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
/* GRAPH */
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative; font-size: 0 }
.graph li { height: 100%; position: absolute; transition: all 0.3s; }
.graph-stacked { white-space: nowrap; }
.graph-stacked li { position: static; display: inline-block; height: 20px }
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
.graph-legend span { position: absolute; }
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
.filelist li { overflow: hidden; text-overflow: ellipsis; }
/* COLORS */
.back-green { background-color: #2ECC71 }
.color-green:before { color: #2ECC71 }
.back-blue { background-color: #3BAFDA }
.color-blue:before { color: #3BAFDA }
.back-darkblue { background-color: #156fb7 }
.color-darkblue:before { color: #156fb7 }
.back-purple { background-color: #B10DC9 }
.color-purple:before { color: #B10DC9 }
.back-yellow { background-color: #FFDC00 }
.color-yellow:before { color: #FFDC00 }
.back-orange { background-color: #FF9800 }
.color-orange:before { color: #FF9800 }
.back-gray { background-color: #ECF0F1 }
.color-gray:before { color: #ECF0F1 }
.back-black { background-color: #34495E }
.color-black:before { color: #34495E }
.back-red { background-color: #5E4934 }
.color-red:before { color: #5E4934 }
.back-gray { background-color: #9e9e9e }
.color-gray:before { color: #9e9e9e }
.back-white { background-color: #EEE }
.color-white:before { color: #EEE }
.back-red { background-color: #E91E63 }
.color-red:before { color: #E91E63 }
/* Settings owned */
.owned-title { float: left }
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
.settings-owned { clear: both }
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden }
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px }
/* Settings autodownload */
.settings-autodownloadoptional { clear: both; box-sizing: border-box; padding-top: 0px; }
#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; transition: all 0.3s ease-in-out; overflow: hidden; }
#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; }
/* Globe */
.globe { width: 360px; height: 360px }
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
.globe.error { text-align: center; padding-top: 156px; box-sizing: border-box; opacity: 0.2; }
/* Sign publish */
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; margin-top: 10px }
.contents a { color: white }
.contents a:active { background-color: #6B6B6B }
.contents + .flex.active {
padding-bottom: 100px;
}
#wrapper-sign-publish {
padding: 0;
}
#button-sign-publish, #menu-sign-publish {
display: inline-block;
margin: 5px 10px;
text-decoration: none;
}
#button-sign-publish {
margin-right: 5px;
}
#menu-sign-publish {
margin-left: 5px;
color: #AAA;
padding: 7px;
margin: 0px;
}
#menu-sign-publish:hover { color: white }
/* Small screen */
@media screen and (max-width: 600px) {
.sidebar .close { display: block }
}

View File

@ -0,0 +1,281 @@
/* ---- Console.css ---- */
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
.console-container .console { background-color: #212121; height: 100vh; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -o-transform: translateY(0px); -ms-transform: translateY(0px); transform: translateY(0px) ; padding-top: 80px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
.console-tabs {
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
-webkit-box-shadow: -30px 0px 45px #7d2463; -moz-box-shadow: -30px 0px 45px #7d2463; -o-box-shadow: -30px 0px 45px #7d2463; -ms-box-shadow: -30px 0px 45px #7d2463; box-shadow: -30px 0px 45px #7d2463 ; background: -webkit-linear-gradient(-75deg, #591a48ed, #70305e66);background: -moz-linear-gradient(-75deg, #591a48ed, #70305e66);background: -o-linear-gradient(-75deg, #591a48ed, #70305e66);background: -ms-linear-gradient(-75deg, #591a48ed, #70305e66);background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
}
.console-tabs a {
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
}
.console-tabs a:hover { color: #FFF }
.console-tabs a.active { background-color: #46223c; color: #FFF }
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
.console .mynode {
border: 0.5px solid #aaa; width: 50px; height: 50px; -webkit-transform: rotateZ(45deg); -moz-transform: rotateZ(45deg); -o-transform: rotateZ(45deg); -ms-transform: rotateZ(45deg); transform: rotateZ(45deg) ; margin-top: -25px; margin-left: -25px;
opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE;
}
.console .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; }
.console .peer { left: 0px; top: 0px; position: absolute; }
.console .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; }
.console .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s }
.console .peer .icon:hover:before { opacity: 1; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none }
.console .peer .line {
width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px;
-webkit-transform: rotateZ(334deg); -moz-transform: rotateZ(334deg); -o-transform: rotateZ(334deg); -ms-transform: rotateZ(334deg); transform: rotateZ(334deg) ; transform-origin: bottom left;
}
/* ---- Menu.css ---- */
.menu {
background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; -webkit-transform: translate(0px, -30px); -moz-transform: translate(0px, -30px); -o-transform: translate(0px, -30px); -ms-transform: translate(0px, -30px); transform: translate(0px, -30px) ; pointer-events: none;
-webkit-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -moz-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -o-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -ms-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); box-shadow: 0px 2px 8px rgba(0,0,0,0.3) ; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; opacity: 0; -webkit-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -moz-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -o-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -ms-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out ;
}
.menu.visible { opacity: 1; max-height: 350px; -webkit-transform: translate(0px, 0px); -moz-transform: translate(0px, 0px); -o-transform: translate(0px, 0px); -ms-transform: translate(0px, 0px); transform: translate(0px, 0px) ; -webkit-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -moz-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -o-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -ms-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out ; pointer-events: all }
.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; -webkit-transition: all 0.2s; -moz-transition: all 0.2s; -o-transition: all 0.2s; -ms-transition: all 0.2s; transition: all 0.2s ; border-bottom: none; font-weight: normal; padding-left: 30px; }
.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee }
.menu-item:hover { background-color: #F6F6F6; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; color: inherit; border: none }
.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none }
.menu-item.selected:before {
content: "L"; display: inline-block; -webkit-transform: rotateZ(45deg) scaleX(-1); -moz-transform: rotateZ(45deg) scaleX(-1); -o-transform: rotateZ(45deg) scaleX(-1); -ms-transform: rotateZ(45deg) scaleX(-1); transform: rotateZ(45deg) scaleX(-1) ;
font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px;
}
@media only screen and (max-width: 800px) {
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
}
/* ---- Scrollbable.css ---- */
.scrollable {
overflow: hidden;
}
.scrollable.showScroll::after {
position: absolute;
content: '';
top: 5%;
right: 7px;
height: 90%;
width: 3px;
background: rgba(224, 224, 255, .3);
}
.scrollable .content-wrapper {
width: 100%;
height: 100%;
padding-right: 50%;
overflow-y: scroll;
}
.scroller {
margin-top: 5px;
z-index: 5;
cursor: pointer;
position: absolute;
width: 7px;
-webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ;
background: #3A3A3A;
top: 0px;
left: 395px;
-webkit-transition: top .08s;
-moz-transition: top .08s;
-ms-transition: top .08s;
-o-transition: top .08s;
-webkit-transition: top .08s; -moz-transition: top .08s; -o-transition: top .08s; -ms-transition: top .08s; transition: top .08s ;
}
.scroller {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ---- Sidebar.css ---- */
.menu {
font-family: Roboto, 'Segoe UI', 'Helvetica Neue'; z-index: 999;
}
.drag-bg { width: 100%; height: 100%; position: fixed; }
.fixbutton.dragging { cursor: -webkit-grabbing; }
.fixbutton-bg:active { cursor: -webkit-grabbing; }
.body-sidebar, .body-console { background-color: #666 !important; }
#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left bottom; }
.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
.body-console iframe { -webkit-transform: rotateX(5deg); -moz-transform: rotateX(5deg); -o-transform: rotateX(5deg); -ms-transform: rotateX(5deg); transform: rotateX(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
.sidebar .label-right { float: right; margin-right: 7px; margin-top: 1px; float: right; }
.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; }
.sidebar .link-right:hover { border-color: #CCC; }
.sidebar .link-right:active { background-color: #444 }
.sidebar .link-outline { outline: 1px solid #eee6; padding: 2px 13px; border-bottom: none; font-size: 80%; }
/* SIDEBAR */
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; top: 0px; z-index: 2;}
.sidebar { background-color: #212121; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 }
.sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none }
.sidebar h1, .sidebar h2 { font-weight: lighter; }
.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none }
.sidebar .button { margin: 0px; display: inline-block; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; max-width: 260px }
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
.sidebar .flex { display: flex }
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
/* FIELDS */
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
.sidebar .fields label {
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
vertical-align: text-bottom; margin-right: 10px; width: 100%
}
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; width: 260px; font-family: Consolas, monospace; }
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
/* Select */
.sidebar .fields select {
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; -ms-appearance: none; appearance: none ;
padding: 5px; padding-right: 25px; border: 0px; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; height: 35px; vertical-align: 1px; -webkit-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -moz-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -o-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -ms-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); box-shadow: 0px 1px 2px rgba(0,0,0,0.5) ;
}
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; -webkit-transform: rotateZ(90deg); -moz-transform: rotateZ(90deg); -o-transform: rotateZ(90deg); -ms-transform: rotateZ(90deg); transform: rotateZ(90deg) ; height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
/* Checkbox */
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; margin-left: -59px; }
.sidebar .fields .checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px;
-webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ;
}
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
/* Fake input */
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
/* GRAPH */
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; -o-border-radius: 8px; -ms-border-radius: 8px; border-radius: 8px ; overflow: hidden; position: relative; font-size: 0 }
.graph li { height: 100%; position: absolute; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; }
.graph-stacked { white-space: nowrap; }
.graph-stacked li { position: static; display: inline-block; height: 20px }
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
.graph-legend span { position: absolute; }
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
.filelist li { overflow: hidden; text-overflow: ellipsis; }
/* COLORS */
.back-green { background-color: #2ECC71 }
.color-green:before { color: #2ECC71 }
.back-blue { background-color: #3BAFDA }
.color-blue:before { color: #3BAFDA }
.back-darkblue { background-color: #156fb7 }
.color-darkblue:before { color: #156fb7 }
.back-purple { background-color: #B10DC9 }
.color-purple:before { color: #B10DC9 }
.back-yellow { background-color: #FFDC00 }
.color-yellow:before { color: #FFDC00 }
.back-orange { background-color: #FF9800 }
.color-orange:before { color: #FF9800 }
.back-gray { background-color: #ECF0F1 }
.color-gray:before { color: #ECF0F1 }
.back-black { background-color: #34495E }
.color-black:before { color: #34495E }
.back-red { background-color: #5E4934 }
.color-red:before { color: #5E4934 }
.back-gray { background-color: #9e9e9e }
.color-gray:before { color: #9e9e9e }
.back-white { background-color: #EEE }
.color-white:before { color: #EEE }
.back-red { background-color: #E91E63 }
.color-red:before { color: #E91E63 }
/* Settings owned */
.owned-title { float: left }
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
.settings-owned { clear: both }
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden }
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px }
/* Settings autodownload */
.settings-autodownloadoptional { clear: both; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; padding-top: 0px; }
#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; overflow: hidden; }
#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; }
/* Globe */
.globe { width: 360px; height: 360px }
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
.globe.error { text-align: center; padding-top: 156px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; opacity: 0.2; }
/* Sign publish */
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; margin-top: 10px }
.contents a { color: white }
.contents a:active { background-color: #6B6B6B }
.contents + .flex.active {
padding-bottom: 100px;
}
#wrapper-sign-publish {
padding: 0;
}
#button-sign-publish, #menu-sign-publish {
display: inline-block;
margin: 5px 10px;
text-decoration: none;
}
#button-sign-publish {
margin-right: 5px;
}
#menu-sign-publish {
margin-left: 5px;
color: #AAA;
padding: 7px;
margin: 0px;
}
#menu-sign-publish:hover { color: white }
/* Small screen */
@media screen and (max-width: 600px) {
.sidebar .close { display: block }
}

1770
plugins/Sidebar/media/all.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,340 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var specialElHandlers = {
/**
* Needed for IE. Apparently IE doesn't think
* that "selected" is an attribute when reading
* over the attributes using selectEl.attributes
*/
OPTION: function(fromEl, toEl) {
if ((fromEl.selected = toEl.selected)) {
fromEl.setAttribute('selected', '');
} else {
fromEl.removeAttribute('selected', '');
}
},
/**
* The "value" attribute is special for the <input> element
* since it sets the initial value. Changing the "value"
* attribute without changing the "value" property will have
* no effect since it is only used to the set the initial value.
* Similar for the "checked" attribute.
*/
/*INPUT: function(fromEl, toEl) {
fromEl.checked = toEl.checked;
fromEl.value = toEl.value;
if (!toEl.hasAttribute('checked')) {
fromEl.removeAttribute('checked');
}
if (!toEl.hasAttribute('value')) {
fromEl.removeAttribute('value');
}
}*/
};
function noop() {}
/**
* Loop over all of the attributes on the target node and make sure the
* original DOM node has the same attributes. If an attribute
* found on the original node is not on the new node then remove it from
* the original node
* @param {HTMLElement} fromNode
* @param {HTMLElement} toNode
*/
function morphAttrs(fromNode, toNode) {
var attrs = toNode.attributes;
var i;
var attr;
var attrName;
var attrValue;
var foundAttrs = {};
for (i=attrs.length-1; i>=0; i--) {
attr = attrs[i];
if (attr.specified !== false) {
attrName = attr.name;
attrValue = attr.value;
foundAttrs[attrName] = true;
if (fromNode.getAttribute(attrName) !== attrValue) {
fromNode.setAttribute(attrName, attrValue);
}
}
}
// Delete any extra attributes found on the original DOM element that weren't
// found on the target element.
attrs = fromNode.attributes;
for (i=attrs.length-1; i>=0; i--) {
attr = attrs[i];
if (attr.specified !== false) {
attrName = attr.name;
if (!foundAttrs.hasOwnProperty(attrName)) {
fromNode.removeAttribute(attrName);
}
}
}
}
/**
* Copies the children of one DOM element to another DOM element
*/
function moveChildren(from, to) {
var curChild = from.firstChild;
while(curChild) {
var nextChild = curChild.nextSibling;
to.appendChild(curChild);
curChild = nextChild;
}
return to;
}
function morphdom(fromNode, toNode, options) {
if (!options) {
options = {};
}
if (typeof toNode === 'string') {
var newBodyEl = document.createElement('body');
newBodyEl.innerHTML = toNode;
toNode = newBodyEl.childNodes[0];
}
var savedEls = {}; // Used to save off DOM elements with IDs
var unmatchedEls = {};
var onNodeDiscarded = options.onNodeDiscarded || noop;
var onBeforeMorphEl = options.onBeforeMorphEl || noop;
var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
function removeNodeHelper(node, nestedInSavedEl) {
var id = node.id;
// If the node has an ID then save it off since we will want
// to reuse it in case the target DOM tree has a DOM element
// with the same ID
if (id) {
savedEls[id] = node;
} else if (!nestedInSavedEl) {
// If we are not nested in a saved element then we know that this node has been
// completely discarded and will not exist in the final DOM.
onNodeDiscarded(node);
}
if (node.nodeType === 1) {
var curChild = node.firstChild;
while(curChild) {
removeNodeHelper(curChild, nestedInSavedEl || id);
curChild = curChild.nextSibling;
}
}
}
function walkDiscardedChildNodes(node) {
if (node.nodeType === 1) {
var curChild = node.firstChild;
while(curChild) {
if (!curChild.id) {
// We only want to handle nodes that don't have an ID to avoid double
// walking the same saved element.
onNodeDiscarded(curChild);
// Walk recursively
walkDiscardedChildNodes(curChild);
}
curChild = curChild.nextSibling;
}
}
}
function removeNode(node, parentNode, alreadyVisited) {
parentNode.removeChild(node);
if (alreadyVisited) {
if (!node.id) {
onNodeDiscarded(node);
walkDiscardedChildNodes(node);
}
} else {
removeNodeHelper(node);
}
}
function morphEl(fromNode, toNode, alreadyVisited) {
if (toNode.id) {
// If an element with an ID is being morphed then it is will be in the final
// DOM so clear it out of the saved elements collection
delete savedEls[toNode.id];
}
if (onBeforeMorphEl(fromNode, toNode) === false) {
return;
}
morphAttrs(fromNode, toNode);
if (onBeforeMorphElChildren(fromNode, toNode) === false) {
return;
}
var curToNodeChild = toNode.firstChild;
var curFromNodeChild = fromNode.firstChild;
var curToNodeId;
var fromNextSibling;
var toNextSibling;
var savedEl;
var unmatchedEl;
outer: while(curToNodeChild) {
toNextSibling = curToNodeChild.nextSibling;
curToNodeId = curToNodeChild.id;
while(curFromNodeChild) {
var curFromNodeId = curFromNodeChild.id;
fromNextSibling = curFromNodeChild.nextSibling;
if (!alreadyVisited) {
if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
curFromNodeChild = fromNextSibling;
continue;
}
}
var curFromNodeType = curFromNodeChild.nodeType;
if (curFromNodeType === curToNodeChild.nodeType) {
var isCompatible = false;
if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
if (curFromNodeChild.tagName === curToNodeChild.tagName) {
// We have compatible DOM elements
if (curFromNodeId || curToNodeId) {
// If either DOM element has an ID then we handle
// those differently since we want to match up
// by ID
if (curToNodeId === curFromNodeId) {
isCompatible = true;
}
} else {
isCompatible = true;
}
}
if (isCompatible) {
// We found compatible DOM elements so add a
// task to morph the compatible DOM elements
morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
}
} else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
isCompatible = true;
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
}
if (isCompatible) {
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
}
// No compatible match so remove the old node from the DOM
removeNode(curFromNodeChild, fromNode, alreadyVisited);
curFromNodeChild = fromNextSibling;
}
if (curToNodeId) {
if ((savedEl = savedEls[curToNodeId])) {
morphEl(savedEl, curToNodeChild, true);
curToNodeChild = savedEl; // We want to append the saved element instead
} else {
// The current DOM element in the target tree has an ID
// but we did not find a match in any of the corresponding
// siblings. We just put the target element in the old DOM tree
// but if we later find an element in the old DOM tree that has
// a matching ID then we will replace the target element
// with the corresponding old element and morph the old element
unmatchedEls[curToNodeId] = curToNodeChild;
}
}
// If we got this far then we did not find a candidate match for our "to node"
// and we exhausted all of the children "from" nodes. Therefore, we will just
// append the current "to node" to the end
fromNode.appendChild(curToNodeChild);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
}
// We have processed all of the "to nodes". If curFromNodeChild is non-null then
// we still have some from nodes left over that need to be removed
while(curFromNodeChild) {
fromNextSibling = curFromNodeChild.nextSibling;
removeNode(curFromNodeChild, fromNode, alreadyVisited);
curFromNodeChild = fromNextSibling;
}
var specialElHandler = specialElHandlers[fromNode.tagName];
if (specialElHandler) {
specialElHandler(fromNode, toNode);
}
}
var morphedNode = fromNode;
var morphedNodeType = morphedNode.nodeType;
var toNodeType = toNode.nodeType;
// Handle the case where we are given two DOM nodes that are not
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
if (morphedNodeType === 1) {
if (toNodeType === 1) {
if (morphedNode.tagName !== toNode.tagName) {
onNodeDiscarded(fromNode);
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
}
} else {
// Going from an element node to a text node
return toNode;
}
} else if (morphedNodeType === 3) { // Text node
if (toNodeType === 3) {
morphedNode.nodeValue = toNode.nodeValue;
return morphedNode;
} else {
onNodeDiscarded(fromNode);
// Text node to something else
return toNode;
}
}
morphEl(morphedNode, toNode, false);
// Fire the "onNodeDiscarded" event for any saved elements
// that never found a new home in the morphed DOM
for (var savedElId in savedEls) {
if (savedEls.hasOwnProperty(savedElId)) {
var savedEl = savedEls[savedElId];
onNodeDiscarded(savedEl);
walkDiscardedChildNodes(savedEl);
}
}
if (morphedNode !== fromNode && fromNode.parentNode) {
fromNode.parentNode.replaceChild(morphedNode, fromNode);
}
return morphedNode;
}
module.exports = morphdom;
},{}]},{},[1])(1)
});

View File

@ -0,0 +1,60 @@
/**
* @author alteredq / http://alteredqualia.com/
* @author mr.doob / http://mrdoob.com/
*/
Detector = {
canvas : !! window.CanvasRenderingContext2D,
webgl : ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
workers : !! window.Worker,
fileapi : window.File && window.FileReader && window.FileList && window.Blob,
getWebGLErrorMessage : function () {
var domElement = document.createElement( 'div' );
domElement.style.fontFamily = 'monospace';
domElement.style.fontSize = '13px';
domElement.style.textAlign = 'center';
domElement.style.background = '#eee';
domElement.style.color = '#000';
domElement.style.padding = '1em';
domElement.style.width = '475px';
domElement.style.margin = '5em auto 0';
if ( ! this.webgl ) {
domElement.innerHTML = window.WebGLRenderingContext ? [
'Sorry, your graphics card doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a>'
].join( '\n' ) : [
'Sorry, your browser doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a><br/>',
'Please try with',
'<a href="http://www.google.com/chrome">Chrome</a>, ',
'<a href="http://www.mozilla.com/en-US/firefox/new/">Firefox 4</a> or',
'<a href="http://nightly.webkit.org/">Webkit Nightly (Mac)</a>'
].join( '\n' );
}
return domElement;
},
addGetWebGLMessage : function ( parameters ) {
var parent, id, domElement;
parameters = parameters || {};
parent = parameters.parent !== undefined ? parameters.parent : document.body;
id = parameters.id !== undefined ? parameters.id : 'oldie';
domElement = Detector.getWebGLErrorMessage();
domElement.id = id;
parent.appendChild( domElement );
}
};

View File

@ -0,0 +1,12 @@
// Tween.js - http://github.com/sole/tween.js
var TWEEN=TWEEN||function(){var a,e,c,d,f=[];return{start:function(g){c=setInterval(this.update,1E3/(g||60))},stop:function(){clearInterval(c)},add:function(g){f.push(g)},remove:function(g){a=f.indexOf(g);a!==-1&&f.splice(a,1)},update:function(){a=0;e=f.length;for(d=(new Date).getTime();a<e;)if(f[a].update(d))a++;else{f.splice(a,1);e--}}}}();
TWEEN.Tween=function(a){var e={},c={},d={},f=1E3,g=0,j=null,n=TWEEN.Easing.Linear.EaseNone,k=null,l=null,m=null;this.to=function(b,h){if(h!==null)f=h;for(var i in b)if(a[i]!==null)d[i]=b[i];return this};this.start=function(){TWEEN.add(this);j=(new Date).getTime()+g;for(var b in d)if(a[b]!==null){e[b]=a[b];c[b]=d[b]-a[b]}return this};this.stop=function(){TWEEN.remove(this);return this};this.delay=function(b){g=b;return this};this.easing=function(b){n=b;return this};this.chain=function(b){k=b};this.onUpdate=
function(b){l=b;return this};this.onComplete=function(b){m=b;return this};this.update=function(b){var h,i;if(b<j)return true;b=(b-j)/f;b=b>1?1:b;i=n(b);for(h in c)a[h]=e[h]+c[h]*i;l!==null&&l.call(a,i);if(b==1){m!==null&&m.call(a);k!==null&&k.start();return false}return true}};TWEEN.Easing={Linear:{},Quadratic:{},Cubic:{},Quartic:{},Quintic:{},Sinusoidal:{},Exponential:{},Circular:{},Elastic:{},Back:{},Bounce:{}};TWEEN.Easing.Linear.EaseNone=function(a){return a};
TWEEN.Easing.Quadratic.EaseIn=function(a){return a*a};TWEEN.Easing.Quadratic.EaseOut=function(a){return-a*(a-2)};TWEEN.Easing.Quadratic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a;return-0.5*(--a*(a-2)-1)};TWEEN.Easing.Cubic.EaseIn=function(a){return a*a*a};TWEEN.Easing.Cubic.EaseOut=function(a){return--a*a*a+1};TWEEN.Easing.Cubic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a;return 0.5*((a-=2)*a*a+2)};TWEEN.Easing.Quartic.EaseIn=function(a){return a*a*a*a};
TWEEN.Easing.Quartic.EaseOut=function(a){return-(--a*a*a*a-1)};TWEEN.Easing.Quartic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a;return-0.5*((a-=2)*a*a*a-2)};TWEEN.Easing.Quintic.EaseIn=function(a){return a*a*a*a*a};TWEEN.Easing.Quintic.EaseOut=function(a){return(a-=1)*a*a*a*a+1};TWEEN.Easing.Quintic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a*a;return 0.5*((a-=2)*a*a*a*a+2)};TWEEN.Easing.Sinusoidal.EaseIn=function(a){return-Math.cos(a*Math.PI/2)+1};
TWEEN.Easing.Sinusoidal.EaseOut=function(a){return Math.sin(a*Math.PI/2)};TWEEN.Easing.Sinusoidal.EaseInOut=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};TWEEN.Easing.Exponential.EaseIn=function(a){return a==0?0:Math.pow(2,10*(a-1))};TWEEN.Easing.Exponential.EaseOut=function(a){return a==1?1:-Math.pow(2,-10*a)+1};TWEEN.Easing.Exponential.EaseInOut=function(a){if(a==0)return 0;if(a==1)return 1;if((a*=2)<1)return 0.5*Math.pow(2,10*(a-1));return 0.5*(-Math.pow(2,-10*(a-1))+2)};
TWEEN.Easing.Circular.EaseIn=function(a){return-(Math.sqrt(1-a*a)-1)};TWEEN.Easing.Circular.EaseOut=function(a){return Math.sqrt(1- --a*a)};TWEEN.Easing.Circular.EaseInOut=function(a){if((a/=0.5)<1)return-0.5*(Math.sqrt(1-a*a)-1);return 0.5*(Math.sqrt(1-(a-=2)*a)+1)};TWEEN.Easing.Elastic.EaseIn=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return-(c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d))};
TWEEN.Easing.Elastic.EaseOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return c*Math.pow(2,-10*a)*Math.sin((a-e)*2*Math.PI/d)+1};
TWEEN.Easing.Elastic.EaseInOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);if((a*=2)<1)return-0.5*c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d);return c*Math.pow(2,-10*(a-=1))*Math.sin((a-e)*2*Math.PI/d)*0.5+1};TWEEN.Easing.Back.EaseIn=function(a){return a*a*(2.70158*a-1.70158)};TWEEN.Easing.Back.EaseOut=function(a){return(a-=1)*a*(2.70158*a+1.70158)+1};
TWEEN.Easing.Back.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*(3.5949095*a-2.5949095);return 0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)};TWEEN.Easing.Bounce.EaseIn=function(a){return 1-TWEEN.Easing.Bounce.EaseOut(1-a)};TWEEN.Easing.Bounce.EaseOut=function(a){return(a/=1)<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375};
TWEEN.Easing.Bounce.EaseInOut=function(a){if(a<0.5)return TWEEN.Easing.Bounce.EaseIn(a*2)*0.5;return TWEEN.Easing.Bounce.EaseOut(a*2-1)*0.5+0.5};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,436 @@
/**
* dat.globe Javascript WebGL Globe Toolkit
* http://dataarts.github.com/dat.globe
*
* Copyright 2011 Data Arts Team, Google Creative Lab
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
var DAT = DAT || {};
DAT.Globe = function(container, opts) {
opts = opts || {};
var colorFn = opts.colorFn || function(x) {
var c = new THREE.Color();
c.setHSL( ( 0.5 - (x * 2) ), Math.max(0.8, 1.0 - (x * 3)), 0.5 );
return c;
};
var imgDir = opts.imgDir || '/globe/';
var Shaders = {
'earth' : {
uniforms: {
'texture': { type: 't', value: null }
},
vertexShader: [
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'vNormal = normalize( normalMatrix * normal );',
'vUv = uv;',
'}'
].join('\n'),
fragmentShader: [
'uniform sampler2D texture;',
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'vec3 diffuse = texture2D( texture, vUv ).xyz;',
'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );',
'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );',
'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );',
'}'
].join('\n')
},
'atmosphere' : {
uniforms: {},
vertexShader: [
'varying vec3 vNormal;',
'void main() {',
'vNormal = normalize( normalMatrix * normal );',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'}'
].join('\n'),
fragmentShader: [
'varying vec3 vNormal;',
'void main() {',
'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );',
'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;',
'}'
].join('\n')
}
};
var camera, scene, renderer, w, h;
var mesh, atmosphere, point, running;
var overRenderer;
var running = true;
var curZoomSpeed = 0;
var zoomSpeed = 50;
var mouse = { x: 0, y: 0 }, mouseOnDown = { x: 0, y: 0 };
var rotation = { x: 0, y: 0 },
target = { x: Math.PI*3/2, y: Math.PI / 6.0 },
targetOnDown = { x: 0, y: 0 };
var distance = 100000, distanceTarget = 100000;
var padding = 10;
var PI_HALF = Math.PI / 2;
function init() {
container.style.color = '#fff';
container.style.font = '13px/20px Arial, sans-serif';
var shader, uniforms, material;
w = container.offsetWidth || window.innerWidth;
h = container.offsetHeight || window.innerHeight;
camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000);
camera.position.z = distance;
scene = new THREE.Scene();
var geometry = new THREE.SphereGeometry(200, 40, 30);
shader = Shaders['earth'];
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
uniforms['texture'].value = THREE.ImageUtils.loadTexture(imgDir+'world.jpg');
material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
});
mesh = new THREE.Mesh(geometry, material);
mesh.rotation.y = Math.PI;
scene.add(mesh);
shader = Shaders['atmosphere'];
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true
});
mesh = new THREE.Mesh(geometry, material);
mesh.scale.set( 1.1, 1.1, 1.1 );
scene.add(mesh);
geometry = new THREE.BoxGeometry(2.75, 2.75, 1);
geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,0,-0.5));
point = new THREE.Mesh(geometry);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(w, h);
renderer.setClearColor( 0x212121, 1 );
renderer.domElement.style.position = 'relative';
container.appendChild(renderer.domElement);
container.addEventListener('mousedown', onMouseDown, false);
if ('onwheel' in document) {
container.addEventListener('wheel', onMouseWheel, false);
} else {
container.addEventListener('mousewheel', onMouseWheel, false);
}
document.addEventListener('keydown', onDocumentKeyDown, false);
window.addEventListener('resize', onWindowResize, false);
container.addEventListener('mouseover', function() {
overRenderer = true;
}, false);
container.addEventListener('mouseout', function() {
overRenderer = false;
}, false);
}
function addData(data, opts) {
var lat, lng, size, color, i, step, colorFnWrapper;
opts.animated = opts.animated || false;
this.is_animated = opts.animated;
opts.format = opts.format || 'magnitude'; // other option is 'legend'
if (opts.format === 'magnitude') {
step = 3;
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
} else if (opts.format === 'legend') {
step = 4;
colorFnWrapper = function(data, i) { return colorFn(data[i+3]); }
} else if (opts.format === 'peer') {
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
} else {
throw('error: format not supported: '+opts.format);
}
if (opts.animated) {
if (this._baseGeometry === undefined) {
this._baseGeometry = new THREE.Geometry();
for (i = 0; i < data.length; i += step) {
lat = data[i];
lng = data[i + 1];
// size = data[i + 2];
color = colorFnWrapper(data,i);
size = 0;
addPoint(lat, lng, size, color, this._baseGeometry);
}
}
if(this._morphTargetId === undefined) {
this._morphTargetId = 0;
} else {
this._morphTargetId += 1;
}
opts.name = opts.name || 'morphTarget'+this._morphTargetId;
}
var subgeo = new THREE.Geometry();
for (i = 0; i < data.length; i += step) {
lat = data[i];
lng = data[i + 1];
color = colorFnWrapper(data,i);
size = data[i + 2];
size = size*200;
addPoint(lat, lng, size, color, subgeo);
}
if (opts.animated) {
this._baseGeometry.morphTargets.push({'name': opts.name, vertices: subgeo.vertices});
} else {
this._baseGeometry = subgeo;
}
};
function createPoints() {
if (this._baseGeometry !== undefined) {
if (this.is_animated === false) {
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors,
morphTargets: false
}));
} else {
if (this._baseGeometry.morphTargets.length < 8) {
console.log('t l',this._baseGeometry.morphTargets.length);
var padding = 8-this._baseGeometry.morphTargets.length;
console.log('padding', padding);
for(var i=0; i<=padding; i++) {
console.log('padding',i);
this._baseGeometry.morphTargets.push({'name': 'morphPadding'+i, vertices: this._baseGeometry.vertices});
}
}
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors,
morphTargets: true
}));
}
scene.add(this.points);
}
}
function addPoint(lat, lng, size, color, subgeo) {
var phi = (90 - lat) * Math.PI / 180;
var theta = (180 - lng) * Math.PI / 180;
point.position.x = 200 * Math.sin(phi) * Math.cos(theta);
point.position.y = 200 * Math.cos(phi);
point.position.z = 200 * Math.sin(phi) * Math.sin(theta);
point.lookAt(mesh.position);
point.scale.z = Math.max( size, 0.1 ); // avoid non-invertible matrix
point.updateMatrix();
for (var i = 0; i < point.geometry.faces.length; i++) {
point.geometry.faces[i].color = color;
}
if(point.matrixAutoUpdate){
point.updateMatrix();
}
subgeo.merge(point.geometry, point.matrix);
}
function onMouseDown(event) {
event.preventDefault();
container.addEventListener('mousemove', onMouseMove, false);
container.addEventListener('mouseup', onMouseUp, false);
container.addEventListener('mouseout', onMouseOut, false);
mouseOnDown.x = - event.clientX;
mouseOnDown.y = event.clientY;
targetOnDown.x = target.x;
targetOnDown.y = target.y;
container.style.cursor = 'move';
}
function onMouseMove(event) {
mouse.x = - event.clientX;
mouse.y = event.clientY;
var zoomDamp = distance/1000;
target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
target.y = target.y > PI_HALF ? PI_HALF : target.y;
target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
}
function onMouseUp(event) {
container.removeEventListener('mousemove', onMouseMove, false);
container.removeEventListener('mouseup', onMouseUp, false);
container.removeEventListener('mouseout', onMouseOut, false);
container.style.cursor = 'auto';
}
function onMouseOut(event) {
container.removeEventListener('mousemove', onMouseMove, false);
container.removeEventListener('mouseup', onMouseUp, false);
container.removeEventListener('mouseout', onMouseOut, false);
}
function onMouseWheel(event) {
if (container.style.cursor != "move") return false;
event.preventDefault();
if (overRenderer) {
if (event.deltaY) {
zoom(-event.deltaY * (event.deltaMode == 0 ? 1 : 50));
} else {
zoom(event.wheelDeltaY * 0.3);
}
}
return false;
}
function onDocumentKeyDown(event) {
switch (event.keyCode) {
case 38:
zoom(100);
event.preventDefault();
break;
case 40:
zoom(-100);
event.preventDefault();
break;
}
}
function onWindowResize( event ) {
camera.aspect = container.offsetWidth / container.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize( container.offsetWidth, container.offsetHeight );
}
function zoom(delta) {
distanceTarget -= delta;
distanceTarget = distanceTarget > 855 ? 855 : distanceTarget;
distanceTarget = distanceTarget < 350 ? 350 : distanceTarget;
}
function animate() {
if (!running) return
requestAnimationFrame(animate);
render();
}
function render() {
zoom(curZoomSpeed);
rotation.x += (target.x - rotation.x) * 0.1;
rotation.y += (target.y - rotation.y) * 0.1;
distance += (distanceTarget - distance) * 0.3;
camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
camera.position.y = distance * Math.sin(rotation.y);
camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
camera.lookAt(mesh.position);
renderer.render(scene, camera);
}
function unload() {
running = false
container.removeEventListener('mousedown', onMouseDown, false);
if ('onwheel' in document) {
container.removeEventListener('wheel', onMouseWheel, false);
} else {
container.removeEventListener('mousewheel', onMouseWheel, false);
}
document.removeEventListener('keydown', onDocumentKeyDown, false);
window.removeEventListener('resize', onWindowResize, false);
}
init();
this.animate = animate;
this.unload = unload;
this.__defineGetter__('time', function() {
return this._time || 0;
});
this.__defineSetter__('time', function(t) {
var validMorphs = [];
var morphDict = this.points.morphTargetDictionary;
for(var k in morphDict) {
if(k.indexOf('morphPadding') < 0) {
validMorphs.push(morphDict[k]);
}
}
validMorphs.sort();
var l = validMorphs.length-1;
var scaledt = t*l+1;
var index = Math.floor(scaledt);
for (i=0;i<validMorphs.length;i++) {
this.points.morphTargetInfluences[validMorphs[i]] = 0;
}
var lastIndex = index - 1;
var leftover = scaledt - index;
if (lastIndex >= 0) {
this.points.morphTargetInfluences[lastIndex] = 1 - leftover;
}
this.points.morphTargetInfluences[index] = leftover;
this._time = t;
});
this.addData = addData;
this.createPoints = createPoints;
this.renderer = renderer;
this.scene = scene;
return this;
};

814
plugins/Sidebar/media_globe/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1,5 @@
{
"name": "Sidebar",
"description": "Access site management sidebar and console by dragging top-right 0 button to left or down.",
"default": "enabled"
}

View File

@ -0,0 +1,24 @@
import pytest
from TrackerShare import TrackerSharePlugin
from Peer import Peer
from Config import config
@pytest.mark.usefixtures("resetSettings")
@pytest.mark.usefixtures("resetTempSettings")
class TestTrackerShare:
def testAnnounceList(self, file_server):
open("%s/trackers.json" % config.data_dir, "w").write("{}")
tracker_storage = TrackerSharePlugin.tracker_storage
tracker_storage.load()
peer = Peer(file_server.ip, 1544, connection_server=file_server)
assert peer.request("getTrackers")["trackers"] == []
tracker_storage.onTrackerFound("zero://%s:15441" % file_server.ip)
assert peer.request("getTrackers")["trackers"] == []
# It needs to have at least one successfull announce to be shared to other peers
tracker_storage.onTrackerSuccess("zero://%s:15441" % file_server.ip, 1.0)
assert peer.request("getTrackers")["trackers"] == ["zero://%s:15441" % file_server.ip]

View File

@ -0,0 +1,3 @@
from src.Test.conftest import *
from Config import config

View File

@ -0,0 +1,5 @@
[pytest]
python_files = Test*.py
addopts = -rsxX -v --durations=6
markers =
webtest: mark a test as a webtest.

View File

@ -0,0 +1,500 @@
import random
import time
import os
import logging
import json
import atexit
import re
import gevent
from Config import config
from Plugin import PluginManager
from util import helper
class TrackerStorage(object):
def __init__(self):
self.site_announcer = None
self.log = logging.getLogger("TrackerStorage")
self.working_tracker_time_interval = 60 * 60
self.tracker_down_time_interval = 60 * 60
self.tracker_discover_time_interval = 5 * 60
self.shared_trackers_limit_per_protocol = {}
self.shared_trackers_limit_per_protocol["other"] = 2
self.file_path = "%s/shared-trackers.json" % config.data_dir
self.load()
self.time_discover = 0.0
self.time_success = 0.0
atexit.register(self.save)
def initTrackerLimitForProtocol(self):
for s in re.split("[,;]", config.shared_trackers_limit_per_protocol):
x = s.split("=", 1)
if len(x) == 1:
x = ["other", x[0]]
try:
self.shared_trackers_limit_per_protocol[x[0]] = int(x[1])
except ValueError:
pass
self.log.info("Limits per protocol: %s" % self.shared_trackers_limit_per_protocol)
def getTrackerLimitForProtocol(self, protocol):
l = self.shared_trackers_limit_per_protocol
return l.get(protocol, l.get("other"))
def setSiteAnnouncer(self, site_announcer):
if not site_announcer:
return
if not self.site_announcer:
self.site_announcer = site_announcer
self.initTrackerLimitForProtocol()
self.recheckValidTrackers()
else:
self.site_announcer = site_announcer
def isTrackerAddressValid(self, tracker_address):
if not self.site_announcer: # Not completely initialized, skip check
return True
address_parts = self.site_announcer.getAddressParts(tracker_address)
if not address_parts:
self.log.debug("Invalid tracker address: %s" % tracker_address)
return False
handler = self.site_announcer.getTrackerHandler(address_parts["protocol"])
if not handler:
self.log.debug("Invalid tracker address: Unknown protocol %s: %s" % (address_parts["protocol"], tracker_address))
return False
return True
def recheckValidTrackers(self):
trackers = self.getTrackers()
for address, tracker in list(trackers.items()):
if not self.isTrackerAddressValid(address):
del trackers[address]
def isUdpEnabled(self):
if config.disable_udp:
return False
if config.trackers_proxy != "disable":
return False
if config.tor == "always":
return False
return True
def getNormalizedTrackerProtocol(self, tracker_address):
if not self.site_announcer:
return None
address_parts = self.site_announcer.getAddressParts(tracker_address)
if not address_parts:
return None
protocol = address_parts["protocol"]
if protocol == "https":
protocol = "http"
return protocol
def deleteUnusedTrackers(self, supported_trackers):
# If a tracker is in our list, but is absent from the results of getSupportedTrackers(),
# it seems to be supported by software, but forbidden by the settings or network configuration.
# We check and remove thoose trackers here, since onTrackerError() is never emitted for them.
trackers = self.getTrackers()
for tracker_address, tracker in list(trackers.items()):
t = max(trackers[tracker_address]["time_added"],
trackers[tracker_address]["time_success"])
if tracker_address not in supported_trackers and t < time.time() - self.tracker_down_time_interval:
self.log.info("Tracker %s seems to be disabled by the configuration, removing." % tracker_address)
del trackers[tracker_address]
def getSupportedProtocols(self):
if not self.site_announcer:
return None
supported_trackers = self.site_announcer.getSupportedTrackers()
self.deleteUnusedTrackers(supported_trackers)
protocols = set()
for tracker_address in supported_trackers:
protocol = self.getNormalizedTrackerProtocol(tracker_address)
if not protocol:
continue
if protocol == "udp" and not self.isUdpEnabled():
continue
protocols.add(protocol)
protocols = list(protocols)
self.log.debug("Supported tracker protocols: %s" % protocols)
return protocols
def getDefaultFile(self):
return {"trackers": {}}
def onTrackerFound(self, tracker_address, my=False, persistent=False):
if not self.isTrackerAddressValid(tracker_address):
return False
trackers = self.getTrackers()
added = False
if tracker_address not in trackers:
# "My" trackers never get deleted on announce errors, but aren't saved between restarts.
# They are to be used as automatically added addresses from the Bootstrap plugin.
# Persistent trackers never get deleted.
# They are to be used for entries manually added by the user.
# Private trackers never listed to other peer in response of the getTrackers command
trackers[tracker_address] = {
"time_added": time.time(),
"time_success": 0,
"time_error": 0,
"latency": 99.0,
"num_error": 0,
"my": False,
"persistent": False,
"private": False
}
self.log.info("New tracker found: %s" % tracker_address)
added = True
trackers[tracker_address]["time_found"] = time.time()
trackers[tracker_address]["my"] |= my
trackers[tracker_address]["persistent"] |= persistent
return added
def onTrackerSuccess(self, tracker_address, latency):
tracker = self.resolveTracker(tracker_address)
if not tracker:
return
tracker["latency"] = latency
tracker["time_success"] = time.time()
tracker["num_error"] = 0
self.time_success = time.time()
def onTrackerError(self, tracker_address):
tracker = self.resolveTracker(tracker_address)
if not tracker:
return
tracker["time_error"] = time.time()
tracker["num_error"] += 1
self.considerTrackerDeletion(tracker_address)
def considerTrackerDeletion(self, tracker_address):
tracker = self.resolveTracker(tracker_address)
if not tracker:
return
if tracker["my"] or tracker["persistent"]:
return
error_limit = self.getSuccessiveErrorLimit(tracker_address)
if tracker["num_error"] > error_limit:
if self.isTrackerDown(tracker_address):
self.log.info("Tracker %s looks down, removing." % tracker_address)
self.deleteTracker(tracker_address)
elif self.areWayTooManyTrackers(tracker_address):
self.log.info(
"Tracker %s has %d successive errors. Looks like we have too many trackers, so removing." % (
tracker_address,
tracker["num_error"]))
self.deleteTracker(tracker_address)
def areWayTooManyTrackers(self, tracker_address):
# Prevent the tracker list overgrowth by hard limiting the maximum size
protocol = self.getNormalizedTrackerProtocol(tracker_address) or ""
nr_trackers_for_protocol = len(self.getTrackersPerProtocol().get(protocol, []))
nr_trackers = len(self.getTrackers())
hard_limit_mult = 5
hard_limit_for_protocol = self.getTrackerLimitForProtocol(protocol) * hard_limit_mult
hard_limit = config.shared_trackers_limit * hard_limit_mult
if (nr_trackers_for_protocol > hard_limit_for_protocol) and (nr_trackers > hard_limit):
return True
return False
def getSuccessiveErrorLimit(self, tracker_address):
protocol = self.getNormalizedTrackerProtocol(tracker_address) or ""
nr_working_trackers_for_protocol = len(self.getTrackersPerProtocol(working_only=True).get(protocol, []))
nr_working_trackers = len(self.getWorkingTrackers())
error_limit = 30
if nr_working_trackers_for_protocol >= self.getTrackerLimitForProtocol(protocol):
error_limit = 10
if nr_working_trackers >= config.shared_trackers_limit:
error_limit = 5
return error_limit
# Returns the dict of known trackers.
# If condition is None the returned dict can be modified in place, and the
# modifications is reflected in the underlying storage.
# If condition is a function, the dict if filtered by the function,
# and the returned dict has no connection to the underlying storage.
def getTrackers(self, condition = None):
trackers = self.file_content.setdefault("trackers", {})
if condition:
trackers = {
key: tracker for key, tracker in trackers.items()
if condition(key)
}
return trackers
def deleteTracker(self, tracker):
trackers = self.getTrackers()
if isinstance(tracker, str):
if trackers[tracker]:
del trackers[tracker]
else:
trackers.remove(tracker)
def resolveTracker(self, tracker):
if isinstance(tracker, str):
tracker = self.getTrackers().get(tracker, None)
return tracker
def isTrackerDown(self, tracker):
tracker = self.resolveTracker(tracker)
if not tracker:
return False
# Don't consider any trackers down if there haven't been any successful announces at all
if self.time_success < 1:
return False
time_success = max(tracker["time_added"], tracker["time_success"])
time_error = max(tracker["time_added"], tracker["time_error"])
if time_success >= time_error:
return False
# Deadline is calculated based on the time of the last successful announce,
# not based on the current time.
# There may be network connectivity issues, if there haven't been any
# successful announces recently.
deadline = self.time_success - self.tracker_down_time_interval
if time_success >= deadline:
return False
return True
def isTrackerWorking(self, tracker):
tracker = self.resolveTracker(tracker)
if not tracker:
return False
if tracker["time_success"] > time.time() - self.working_tracker_time_interval:
return True
return False
def isTrackerShared(self, tracker):
tracker = self.resolveTracker(tracker)
if not tracker:
return False
if tracker["private"]:
return False
if tracker["my"]:
return True
return self.isTrackerWorking(tracker)
def getWorkingTrackers(self):
return self.getTrackers(self.isTrackerWorking)
def getSharedTrackers(self):
return self.getTrackers(self.isTrackerShared)
def getTrackersPerProtocol(self, working_only=False):
if not self.site_announcer:
return None
trackers = self.getTrackers()
trackers_per_protocol = {}
for tracker_address in trackers:
protocol = self.getNormalizedTrackerProtocol(tracker_address)
if not protocol:
continue
if not working_only or self.isTrackerWorking(tracker_address):
trackers_per_protocol.setdefault(protocol, []).append(tracker_address)
return trackers_per_protocol
def getFileContent(self):
if not os.path.isfile(self.file_path):
open(self.file_path, "w").write("{}")
return self.getDefaultFile()
try:
return json.load(open(self.file_path))
except Exception as err:
self.log.error("Error loading trackers list: %s" % err)
return self.getDefaultFile()
def load(self):
self.file_content = self.getFileContent()
trackers = self.getTrackers()
self.log.debug("Loaded %s shared trackers" % len(trackers))
for address, tracker in list(trackers.items()):
tracker.setdefault("time_added", time.time())
tracker.setdefault("time_success", 0)
tracker.setdefault("time_error", 0)
tracker.setdefault("latency", 99.0)
tracker.setdefault("my", False)
tracker.setdefault("persistent", False)
tracker.setdefault("private", False)
tracker["num_error"] = 0
if tracker["my"]:
del trackers[address]
self.recheckValidTrackers()
def save(self):
s = time.time()
helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8"))
self.log.debug("Saved in %.3fs" % (time.time() - s))
def enoughWorkingTrackers(self):
supported_protocols = self.getSupportedProtocols()
if not supported_protocols:
return False
trackers_per_protocol = self.getTrackersPerProtocol(working_only=True)
if not trackers_per_protocol:
return False
unmet_conditions = 0
total_nr = 0
for protocol in supported_protocols:
trackers = trackers_per_protocol.get(protocol, [])
if len(trackers) < self.getTrackerLimitForProtocol(protocol):
self.log.info("Not enough working trackers for protocol %s: %s < %s" % (
protocol, len(trackers), self.getTrackerLimitForProtocol(protocol)))
unmet_conditions += 1
total_nr += len(trackers)
if total_nr < config.shared_trackers_limit:
self.log.info("Not enough working trackers (total): %s < %s" % (
total_nr, config.shared_trackers_limit))
unmet_conditions + 1
return unmet_conditions == 0
def discoverTrackers(self, peers):
if self.enoughWorkingTrackers():
return False
self.log.info("Discovering trackers from %s peers..." % len(peers))
s = time.time()
num_success = 0
num_trackers_discovered = 0
for peer in peers:
if peer.connection and peer.connection.handshake.get("rev", 0) < 3560:
continue # Not supported
res = peer.request("getTrackers")
if not res or "error" in res:
continue
num_success += 1
random.shuffle(res["trackers"])
for tracker_address in res["trackers"]:
if type(tracker_address) is bytes: # Backward compatibilitys
tracker_address = tracker_address.decode("utf8")
added = self.onTrackerFound(tracker_address)
if added: # Only add one tracker from one source
num_trackers_discovered += 1
break
if not num_success and len(peers) < 20:
self.time_discover = 0.0
if num_success:
self.save()
self.log.info("Discovered %s new trackers from %s/%s peers in %.3fs" % (num_trackers_discovered, num_success, len(peers), time.time() - s))
def checkDiscoveringTrackers(self, peers):
if not peers or len(peers) < 1:
return
now = time.time()
if self.time_discover + self.tracker_discover_time_interval >= now:
return
self.time_discover = now
gevent.spawn(self.discoverTrackers, peers)
if "tracker_storage" not in locals():
tracker_storage = TrackerStorage()
@PluginManager.registerTo("SiteAnnouncer")
class SiteAnnouncerPlugin(object):
def getTrackers(self):
tracker_storage.setSiteAnnouncer(self)
tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers(only_fully_connected=True))
trackers = super(SiteAnnouncerPlugin, self).getTrackers()
shared_trackers = list(tracker_storage.getTrackers().keys())
if shared_trackers:
return trackers + shared_trackers
else:
return trackers
def announceTracker(self, tracker, *args, **kwargs):
tracker_storage.setSiteAnnouncer(self)
res = super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs)
if res:
latency = res
tracker_storage.onTrackerSuccess(tracker, latency)
elif res is False:
tracker_storage.onTrackerError(tracker)
return res
@PluginManager.registerTo("FileRequest")
class FileRequestPlugin(object):
def actionGetTrackers(self, params):
shared_trackers = list(tracker_storage.getSharedTrackers().keys())
random.shuffle(shared_trackers)
self.response({"trackers": shared_trackers})
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("TrackerShare plugin")
group.add_argument('--shared_trackers_limit', help='Discover new shared trackers if this number of shared trackers isn\'t reached (total)', default=20, type=int, metavar='limit')
group.add_argument('--shared_trackers_limit_per_protocol', help='Discover new shared trackers if this number of shared trackers isn\'t reached per each supported protocol', default="zero=10,other=5", metavar='limit')
return super(ConfigPlugin, self).createArguments()

View File

@ -0,0 +1 @@
from . import TrackerSharePlugin

View File

@ -0,0 +1,5 @@
{
"name": "TrackerShare",
"description": "Share possible trackers between clients.",
"default": "enabled"
}

View File

@ -0,0 +1,156 @@
import time
import re
import gevent
from Config import config
from Db import Db
from util import helper
class TrackerZeroDb(Db.Db):
def __init__(self):
self.version = 7
self.hash_ids = {} # hash -> id cache
super(TrackerZeroDb, self).__init__({"db_name": "TrackerZero"}, "%s/tracker-zero.db" % config.data_dir)
self.foreign_keys = True
self.checkTables()
self.updateHashCache()
gevent.spawn(self.cleanup)
def cleanup(self):
while 1:
time.sleep(4 * 60)
timeout = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60 * 40))
self.execute("DELETE FROM peer WHERE date_announced < ?", [timeout])
def updateHashCache(self):
res = self.execute("SELECT * FROM hash")
self.hash_ids = {row["hash"]: row["hash_id"] for row in res}
self.log.debug("Loaded %s hash_ids" % len(self.hash_ids))
def checkTables(self):
version = int(self.execute("PRAGMA user_version").fetchone()[0])
self.log.debug("Db version: %s, needed: %s" % (version, self.version))
if version < self.version:
self.createTables()
else:
self.execute("VACUUM")
def createTables(self):
# Delete all tables
self.execute("PRAGMA writable_schema = 1")
self.execute("DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger')")
self.execute("PRAGMA writable_schema = 0")
self.execute("VACUUM")
self.execute("PRAGMA INTEGRITY_CHECK")
# Create new tables
self.execute("""
CREATE TABLE peer (
peer_id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE,
type TEXT,
address TEXT,
port INTEGER NOT NULL,
date_added DATETIME DEFAULT (CURRENT_TIMESTAMP),
date_announced DATETIME DEFAULT (CURRENT_TIMESTAMP)
);
""")
self.execute("CREATE UNIQUE INDEX peer_key ON peer (address, port);")
self.execute("""
CREATE TABLE peer_to_hash (
peer_to_hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
peer_id INTEGER REFERENCES peer (peer_id) ON DELETE CASCADE,
hash_id INTEGER REFERENCES hash (hash_id)
);
""")
self.execute("CREATE INDEX peer_id ON peer_to_hash (peer_id);")
self.execute("CREATE INDEX hash_id ON peer_to_hash (hash_id);")
self.execute("""
CREATE TABLE hash (
hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
hash BLOB UNIQUE NOT NULL,
date_added DATETIME DEFAULT (CURRENT_TIMESTAMP)
);
""")
self.execute("PRAGMA user_version = %s" % self.version)
def getHashId(self, hash):
if hash not in self.hash_ids:
self.log.debug("New hash: %s" % repr(hash))
self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash})
self.hash_ids[hash] = self.cur.cursor.lastrowid
return self.hash_ids[hash]
def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False):
hashes_ids_announced = []
for hash in hashes:
hashes_ids_announced.append(self.getHashId(hash))
# Check user
res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"address": address, "port": port})
user_row = res.fetchone()
now = time.strftime("%Y-%m-%d %H:%M:%S")
if user_row:
peer_id = user_row["peer_id"]
self.execute("UPDATE peer SET date_announced = ? WHERE peer_id = ?", (now, peer_id))
else:
self.log.debug("New peer: %s signed: %s" % (address, onion_signed))
if ip_type == "onion" and not onion_signed:
return len(hashes)
self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now})
peer_id = self.cur.cursor.lastrowid
# Check user's hashes
res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id})
hash_ids_db = [row["hash_id"] for row in res]
if hash_ids_db != hashes_ids_announced:
hash_ids_added = set(hashes_ids_announced) - set(hash_ids_db)
hash_ids_removed = set(hash_ids_db) - set(hashes_ids_announced)
if ip_type != "onion" or onion_signed:
for hash_id in hash_ids_added:
self.execute("INSERT INTO peer_to_hash ?", {"peer_id": peer_id, "hash_id": hash_id})
if hash_ids_removed and delete_missing_hashes:
self.execute("DELETE FROM peer_to_hash WHERE ?", {"peer_id": peer_id, "hash_id": list(hash_ids_removed)})
return len(hash_ids_added) + len(hash_ids_removed)
else:
return 0
def peerList(self, hash, address=None, onions=[], port=None, limit=30, need_types=["ipv4", "onion"], order=True):
back = {"ipv4": [], "ipv6": [], "onion": []}
if limit == 0:
return back
hashid = self.getHashId(hash)
if order:
order_sql = "ORDER BY date_announced DESC"
else:
order_sql = ""
where_sql = "hash_id = :hashid"
if onions:
onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str]
where_sql += " AND address NOT IN (%s)" % ",".join(onions_escaped)
elif address:
where_sql += " AND NOT (address = :address AND port = :port)"
query = """
SELECT type, address, port
FROM peer_to_hash
LEFT JOIN peer USING (peer_id)
WHERE %s
%s
LIMIT :limit
""" % (where_sql, order_sql)
res = self.execute(query, {"hashid": hashid, "address": address, "port": port, "limit": limit})
for row in res:
if row["type"] in need_types:
if row["type"] == "onion":
packed = helper.packOnionAddress(row["address"], row["port"])
else:
packed = helper.packAddress(str(row["address"]), row["port"])
back[row["type"]].append(packed)
return back

View File

@ -0,0 +1,325 @@
import atexit
import json
import logging
import re
import os
import time
import binascii
from util import helper
from Plugin import PluginManager
from .TrackerZeroDb import TrackerZeroDb
from Crypt import CryptRsa
from Config import config
class TrackerZero(object):
def __init__(self):
self.log = logging.getLogger("TrackerZero")
self.log_once = set()
self.enabled_addresses = []
self.added_onions = set()
self.config_file_path = "%s/tracker-zero.json" % config.data_dir
self.config = None
self.load()
atexit.register(self.save)
def addOnion(self, tor_manager, onion, private_key):
# XXX: TorManager hangs if Tor returns a code different from 250 OK,
# so we keep the list of already added onions to avoid adding them twice.
# TODO: Report to the upstream.
if onion in self.added_onions:
return onion
res = tor_manager.request(
"ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port)
)
match = re.search("ServiceID=([A-Za-z0-9]+)", res)
if match:
onion_address = match.groups()[0]
self.added_onions.add(onion_address)
return onion_address
return None
def logOnce(self, message):
if message in self.log_once:
return
self.log_once.add(message)
self.log.info(message)
def getDefaultConfig(self):
return {
"settings": {
"enable": False,
"enable_only_in_tor_always_mode": True,
"listen_on_public_ips": False,
"listen_on_temporary_onion_address": False,
"listen_on_persistent_onion_address": True
}
}
def readJSON(self, file_path, default_value):
if not os.path.isfile(file_path):
try:
self.writeJSON(file_path, default_value)
except Exception as err:
self.log.error("Error writing %s: %s" % (file_path, err))
return default_value
try:
return json.load(open(file_path))
except Exception as err:
self.log.error("Error loading %s: %s" % (file_path, err))
return default_value
def writeJSON(self, file_path, value):
helper.atomicWrite(file_path, json.dumps(value, indent=2, sort_keys=True).encode("utf8"))
def load(self):
self.config = self.readJSON(self.config_file_path, self.getDefaultConfig())
def save(self):
self.writeJSON(self.config_file_path, self.config)
def checkOnionSigns(self, onions, onion_signs, onion_sign_this):
if not onion_signs or len(onion_signs) != len(set(onions)):
return False
if time.time() - float(onion_sign_this) > 3 * 60:
return False # Signed out of allowed 3 minutes
onions_signed = []
# Check onion signs
for onion_publickey, onion_sign in onion_signs.items():
if CryptRsa.verify(onion_sign_this.encode(), onion_publickey, onion_sign):
onions_signed.append(CryptRsa.publickeyToOnion(onion_publickey))
else:
break
# Check if the same onion addresses signed as the announced onces
if sorted(onions_signed) == sorted(set(onions)):
return True
else:
return False
def actionAnnounce(self, file_request, params):
if len(self.enabled_addresses) < 1:
file_request.actionUnknown("announce", params)
return
time_started = time.time()
s = time.time()
# Backward compatibility
if "ip4" in params["add"]:
params["add"].append("ipv4")
if "ip4" in params["need_types"]:
params["need_types"].append("ipv4")
hashes = params["hashes"]
all_onions_signed = self.checkOnionSigns(params.get("onions", []), params.get("onion_signs"), params.get("onion_sign_this"))
time_onion_check = time.time() - s
connection_server = file_request.server
ip_type = connection_server.getIpType(file_request.connection.ip)
if ip_type == "onion" or file_request.connection.ip in config.ip_local:
is_port_open = False
elif ip_type in params["add"]:
is_port_open = True
else:
is_port_open = False
s = time.time()
# Separatley add onions to sites or at once if no onions present
i = 0
onion_to_hash = {}
for onion in params.get("onions", []):
if onion not in onion_to_hash:
onion_to_hash[onion] = []
onion_to_hash[onion].append(hashes[i])
i += 1
hashes_changed = 0
for onion, onion_hashes in onion_to_hash.items():
hashes_changed += db.peerAnnounce(
ip_type="onion",
address=onion,
port=params["port"],
hashes=onion_hashes,
onion_signed=all_onions_signed
)
time_db_onion = time.time() - s
s = time.time()
if is_port_open:
hashes_changed += db.peerAnnounce(
ip_type=ip_type,
address=file_request.connection.ip,
port=params["port"],
hashes=hashes,
delete_missing_hashes=params.get("delete")
)
time_db_ip = time.time() - s
s = time.time()
# Query sites
back = {}
peers = []
if params.get("onions") and not all_onions_signed and hashes_changed:
back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing
if len(hashes) > 500 or not hashes_changed:
limit = 5
order = False
else:
limit = 30
order = True
for hash in hashes:
if time.time() - time_started > 1: # 1 sec limit on request
file_request.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes)))
break
hash_peers = db.peerList(
hash,
address=file_request.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"],
limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order
)
if "ip4" in params["need_types"]: # Backward compatibility
hash_peers["ip4"] = hash_peers["ipv4"]
del(hash_peers["ipv4"])
peers.append(hash_peers)
time_peerlist = time.time() - s
back["peers"] = peers
file_request.connection.log(
"Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" %
(len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit)
)
file_request.response(back)
def getTrackerStorage(self):
try:
if "TrackerShare" in PluginManager.plugin_manager.plugin_names:
from TrackerShare.TrackerSharePlugin import tracker_storage
return tracker_storage
elif "AnnounceShare" in PluginManager.plugin_manager.plugin_names:
from AnnounceShare.AnnounceSharePlugin import tracker_storage
return tracker_storage
except Exception as err:
self.log.error("%s" % Debug.formatException(err))
return None
def registerTrackerAddress(self, message, address, port):
_tracker_storage = self.getTrackerStorage()
if not _tracker_storage:
return
my_tracker_address = "zero://%s:%s" % (address, port)
if _tracker_storage.onTrackerFound(my_tracker_address, my=True):
self.logOnce("listening on %s: %s" % (message, my_tracker_address))
self.enabled_addresses.append("%s:%s" % (address, port))
def registerTrackerAddresses(self, file_server, port_open):
_tracker_storage = self.getTrackerStorage()
if not _tracker_storage:
return
tor_manager = file_server.tor_manager
settings = self.config.get("settings", {})
if not settings.get("enable"):
self.logOnce("Plugin loaded, but disabled by the settings")
return False
if settings.get("enable_only_in_tor_always_mode") and not config.tor == "always":
self.logOnce("Plugin loaded, but disabled from running in the modes other than 'tor = always'")
return False
self.enabled_addresses = []
if settings.get("listen_on_public_ips") and port_open and not config.tor == "always":
for ip in file_server.ip_external_list:
self.registerTrackerAddress("public IP", ip, config.fileserver_port)
if settings.get("listen_on_temporary_onion_address") and tor_manager.enabled:
onion = tor_manager.getOnion(config.homepage)
if onion:
self.registerTrackerAddress("temporary onion address", "%s.onion" % onion, tor_manager.fileserver_port)
if settings.get("listen_on_persistent_onion_address") and tor_manager.enabled:
persistent_addresses = self.config.setdefault("persistent_addresses", {})
if len(persistent_addresses) == 0:
result = tor_manager.makeOnionAndKey()
if result:
onion_address, onion_privatekey = result
persistent_addresses[onion_address] = {
"private_key": onion_privatekey
}
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
else:
for address, d in persistent_addresses.items():
private_key = d.get("private_key")
if not private_key:
continue
onion_address = self.addOnion(tor_manager, address, private_key)
if onion_address == address:
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
return len(self.enabled_addresses) > 0
if "db" not in locals().keys(): # Share during reloads
db = TrackerZeroDb()
if "tracker_zero" not in locals():
tracker_zero = TrackerZero()
@PluginManager.registerTo("FileRequest")
class FileRequestPlugin(object):
def actionAnnounce(self, params):
tracker_zero.actionAnnounce(self, params)
@PluginManager.registerTo("FileServer")
class FileServerPlugin(object):
def portCheck(self, *args, **kwargs):
res = super(FileServerPlugin, self).portCheck(*args, **kwargs)
tracker_zero.registerTrackerAddresses(self, res)
return res
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
@helper.encodeResponse
def actionStatsTrackerZero(self):
self.sendHeader()
# Style
yield """
<style>
* { font-family: monospace; white-space: pre }
table td, table th { text-align: right; padding: 0px 10px }
</style>
"""
hash_rows = db.execute("SELECT * FROM hash").fetchall()
for hash_row in hash_rows:
peer_rows = db.execute(
"SELECT * FROM peer LEFT JOIN peer_to_hash USING (peer_id) WHERE hash_id = :hash_id",
{"hash_id": hash_row["hash_id"]}
).fetchall()
yield "<br>%s (added: %s, peers: %s)<br>" % (
binascii.hexlify(hash_row["hash"]).decode("utf-8"), hash_row["date_added"], len(peer_rows)
)
for peer_row in peer_rows:
yield " - {type: <6} {address: <30} {port: >5} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))

View File

@ -0,0 +1 @@
from . import TrackerZeroPlugin

View File

@ -0,0 +1,80 @@
import time
from Plugin import PluginManager
from Translate import translate
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionSiteMedia(self, path, **kwargs):
file_name = path.split("/")[-1].lower()
if not file_name: # Path ends with /
file_name = "index.html"
extension = file_name.split(".")[-1]
if extension == "html": # Always replace translate variables in html files
should_translate = True
elif extension == "js" and translate.lang != "en":
should_translate = True
else:
should_translate = False
if should_translate:
path_parts = self.parsePath(path)
kwargs["header_length"] = False
file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
if "__next__" in dir(file_generator): # File found and generator returned
site = self.server.sites.get(path_parts["address"])
if not site or not site.content_manager.contents.get("content.json"):
return file_generator
return self.actionPatchFile(site, path_parts["inner_path"], file_generator)
else:
return file_generator
else:
return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
def actionUiMedia(self, path):
file_generator = super(UiRequestPlugin, self).actionUiMedia(path)
if translate.lang != "en" and path.endswith(".js"):
s = time.time()
data = b"".join(list(file_generator))
data = translate.translateData(data.decode("utf8"))
self.log.debug("Patched %s (%s bytes) in %.3fs" % (path, len(data), time.time() - s))
return iter([data.encode("utf8")])
else:
return file_generator
def actionPatchFile(self, site, inner_path, file_generator):
content_json = site.content_manager.contents.get("content.json")
lang_file = "languages/%s.json" % translate.lang
lang_file_exist = False
if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing)
if site.storage.isFile(lang_file):
lang_file_exist = True
else: # Not my site the reference in content.json is enough (will wait for download later)
if lang_file in content_json.get("files", {}):
lang_file_exist = True
if not lang_file_exist or inner_path not in content_json.get("translate", []):
for part in file_generator:
if inner_path.endswith(".html"):
yield part.replace(b"lang={lang}", b"lang=" + translate.lang.encode("utf8")) # lang get parameter to .js file to avoid cache
else:
yield part
else:
s = time.time()
data = b"".join(list(file_generator)).decode("utf8")
# if site.content_manager.contents["content.json"]["files"].get(lang_file):
site.needFile(lang_file, priority=10)
try:
if inner_path.endswith("js"):
data = translate.translateData(data, site.storage.loadJson(lang_file), "js")
else:
data = translate.translateData(data, site.storage.loadJson(lang_file), "html")
except Exception as err:
site.log.error("Error loading translation file %s: %s" % (lang_file, err))
self.log.debug("Patched %s (%s bytes) in %.3fs" % (inner_path, len(data), time.time() - s))
yield data.encode("utf8")

View File

@ -0,0 +1 @@
from . import TranslateSitePlugin

View File

@ -0,0 +1,5 @@
{
"name": "TranslateSite",
"description": "Transparent support translation of site javascript and html files.",
"default": "enabled"
}

View File

@ -0,0 +1,72 @@
import io
import os
from Plugin import PluginManager
from Config import config
from Translate import Translate
from util.Flag import flag
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionWrapper(self, path, extra_headers=None):
if path.strip("/") != "Config":
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
if not extra_headers:
extra_headers = {}
script_nonce = self.getScriptNonce()
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
site = self.server.site_manager.get(config.homepage)
return iter([super(UiRequestPlugin, self).renderWrapper(
site, path, "uimedia/plugins/uiconfig/config.html",
"Config", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
)])
def actionUiMedia(self, path, *args, **kwargs):
if path.startswith("/uimedia/plugins/uiconfig/"):
file_path = path.replace("/uimedia/plugins/uiconfig/", plugin_dir + "/media/")
if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(file_path)
if file_path.endswith("js"):
data = _.translateData(open(file_path).read(), mode="js").encode("utf8")
elif file_path.endswith("html"):
data = _.translateData(open(file_path).read(), mode="html").encode("utf8")
else:
data = open(file_path, "rb").read()
return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data))
else:
return super(UiRequestPlugin, self).actionUiMedia(path)
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
@flag.admin
def actionConfigList(self, to):
back = {}
config_values = vars(config.arguments)
config_values.update(config.pending_changes)
for key, val in config_values.items():
if key not in config.keys_api_change_allowed:
continue
is_pending = key in config.pending_changes
if val is None and is_pending:
val = config.parser.get_default(key)
back[key] = {
"value": val,
"default": config.parser.get_default(key),
"pending": is_pending
}
return back

View File

@ -0,0 +1 @@
from . import UiConfigPlugin

View File

@ -0,0 +1,33 @@
{
"ZeroNet config": "ZeroNet beállítások",
"Web Interface": "Web felület",
"Open web browser on ZeroNet startup": "Bögésző megnyitása a ZeroNet indulásakor",
"Network": "Hálózat",
"File server port": "FIle szerver port",
"Other peers will use this port to reach your served sites. (default: 15441)": "Más kliensek ezen a porton tudják elérni a kiszolgált oldalaidat (alapbeállítás: 15441)",
"Disable: Don't connect to peers on Tor network": "Kikapcsolás: Ne csatlakozzon a Tor hálózatra",
"Enable: Only use Tor for Tor network peers": "Bekapcsolás: Csak a Tor kliensekhez használja a Tor hálózatot",
"Always: Use Tor for every connections to hide your IP address (slower)": "Mindig: Minden kapcsolatot a Tor hálózaton keresztül hozza létre az IP cím elrejtéséhez (lassabb)",
"Disable": "Kikapcsolás",
"Enable": "Bekapcsolás",
"Always": "Mindig",
"Use Tor bridges": "Tor bridge-ek használata",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Tor elrejtő bridge-ek használata a hálózat szintű Tor tiltás megkerüléséhez (még lassabb)",
"Discover new peers using these adresses": "Új kapcsolat felfedezése ezen címek használatával",
"Trackers files": "Tracker file-ok",
"Load additional list of torrent trackers dynamically, from a file": "További trackerek felfedezése dinamikusan, ezen file használatával",
"Eg.: data/trackers.json": "Pl.: data/trackers.json",
"Proxy for tracker connections": "Proxy tracker kapcsolatohoz",
" configuration item value changed": " beállítás megváltoztatva",
"Save settings": "Beállítások mentése",
"Some changed settings requires restart": "A beállítások érvényesítéséhez a kliens újraindítása szükséges",
"Restart ZeroNet client": "ZeroNet kliens újraindítása"
}

View File

@ -0,0 +1,62 @@
{
"ZeroNet config": "ZeroNetの設定",
"Web Interface": "WEBインターフェース",
"Open web browser on ZeroNet startup": "ZeroNet起動時に自動でブラウザーを開く",
"Network": "ネットワーク",
"Offline mode": "オフラインモード",
"Disable network communication.": "通信を無効化します",
"File server network": "ファイルサーバネットワーク",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "IPv4とIPv6からの受信を許可既定: 両方)",
"Dual (IPv4 & IPv6)": "両方 IPv4 & IPv6",
"File server port": "ファイルサーバのポート",
"Other peers will use this port to reach your served sites. (default: randomize)": "他のピアはこのポートを使用してあなたが所持しているサイトにアクセスします (既定: ランダム)",
"File server external ip": "ファイルサーバの外部IP",
"Detect automatically": "自動検出",
"Your file server is accessible on these ips. (default: detect automatically)": "あなたのファイルサーバへはここで設定したIPでアクセスできます (既定: 自動検出)",
"Disable: Don't connect to peers on Tor network": "無効: Torネットワーク上のピアに接続しない",
"Enable: Only use Tor for Tor network peers": "有効: Torネットワーク上のピアに対してのみTorを使って接続する",
"Always: Use Tor for every connections to hide your IP address (slower)": "常時: 全ての接続にTorを使いIPを秘匿する低速",
"Disable": "無効",
"Enable": "有効",
"Always": "常時",
"Use Tor bridges": "Torブリッジを使用",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "難読化されたブリッジリレーを使用してネットワークレベルのTorブロックを避ける超低速",
"Discover new peers using these adresses": "ここで設定したアドレスを用いてピアを発見します",
"Trackers files": "トラッカーファイル",
"Load additional list of torrent trackers dynamically, from a file": "ファイルからトレントラッカーの追加リストを動的に読み込みます",
"Eg.: data/trackers.json": "例: data/trackers.json",
"Proxy for tracker connections": "トラッカーへの接続に使うプロキシ",
"Custom": "カスタム",
"Custom socks proxy address for trackers": "トラッカーに接続するためのカスタムsocksプロキシのアドレス",
"Performance": "性能",
"Level of logging to file": "ログレベル",
"Everything": "全て",
"Only important messages": "重要なメッセージのみ",
"Only errors": "エラーのみ",
"Threads for async file system reads": "非同期ファイルシステムの読み込みに使うスレッド",
"Threads for async file system writes": "非同期ファイルシステムの書き込みに使うスレッド",
"Threads for cryptographic functions": "暗号機能に使うスレッド",
"Threads for database operations": "データベースの操作に使うスレッド",
"Sync read": "同期読み取り",
"Sync write": "同期書き込み",
"Sync execution": "同期実行",
"1 thread": "1スレッド",
"2 threads": "2スレッド",
"3 threads": "3スレッド",
"4 threads": "4スレッド",
"5 threads": "5スレッド",
"10 threads": "10スレッド",
" configuration item value changed": " の項目の値が変更されました",
"Save settings": "設定を保存",
"Some changed settings requires restart": "一部の変更の適用には再起動が必要です。",
"Restart ZeroNet client": "ZeroNetクライアントを再起動"
}

View File

@ -0,0 +1,62 @@
{
"ZeroNet config": "Konfiguracja ZeroNet",
"Web Interface": "Interfejs webowy",
"Open web browser on ZeroNet startup": "Otwórz przeglądarkę podczas uruchomienia ZeroNet",
"Network": "Sieć",
"Offline mode": "Tryb offline",
"Disable network communication.": "Wyłącz komunikacje sieciową",
"File server network": "Sieć serwera plików",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Akceptuj połączenia przychodzące używając IPv4 i IPv6. (domyślnie: oba)",
"Dual (IPv4 & IPv6)": "Oba (IPv4 i IPv6)",
"File server port": "Port serwera plików",
"Other peers will use this port to reach your served sites. (default: 15441)": "Inni użytkownicy będą używać tego portu do połączenia się z Tobą. (domyślnie 15441)",
"File server external ip": "Zewnętrzny adres IP serwera plików",
"Detect automatically": "Wykryj automatycznie",
"Your file server is accessible on these ips. (default: detect automatically)": "Twój serwer plików będzie dostępny na tych adresach IP. (domyślnie: wykryj automatycznie)",
"Disable: Don't connect to peers on Tor network": "Wyłącz: Nie łącz się do użytkowników sieci Tor",
"Enable: Only use Tor for Tor network peers": "Włącz: Łącz się do użytkowników sieci Tor",
"Always: Use Tor for every connections to hide your IP address (slower)": "Zawsze Tor: Użyj Tor dla wszystkich połączeń w celu ukrycia Twojego adresu IP (wolne działanie)",
"Disable": "Wyłącz",
"Enable": "Włącz",
"Always": "Zawsze Tor",
"Use Tor bridges": "Użyj Tor bridges",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Użyj obfuskacji, aby uniknąć blokowania Tor na poziomie sieci (jeszcze wolniejsze działanie)",
"Trackers": "Trackery",
"Discover new peers using these adresses": "Wykryj użytkowników korzystając z tych adresów trackerów",
"Trackers files": "Pliki trackerów",
"Load additional list of torrent trackers dynamically, from a file": "Dynamicznie wczytaj dodatkową listę trackerów z pliku .json",
"Eg.: data/trackers.json": "Np.: data/trackers.json",
"Proxy for tracker connections": "Serwer proxy dla trackerów",
"Custom": "Własny",
"Custom socks proxy address for trackers": "Adres serwera proxy do łączenia z trackerami",
"Eg.: 127.0.0.1:1080": "Np.: 127.0.0.1:1080",
"Performance": "Wydajność",
"Level of logging to file": "Poziom logowania do pliku",
"Everything": "Wszystko",
"Only important messages": "Tylko ważne wiadomości",
"Only errors": "Tylko błędy",
"Threads for async file system reads": "Wątki asynchroniczne dla odczytu",
"Threads for async file system writes": "Wątki asynchroniczne dla zapisu",
"Threads for cryptographic functions": "Wątki dla funkcji kryptograficznych",
"Threads for database operations": "Wątki dla operacji na bazie danych",
"Sync read": "Synchroniczny odczyt",
"Sync write": "Synchroniczny zapis",
"Sync execution": "Synchroniczne wykonanie",
"1 thread": "1 wątek",
"2 threads": "2 wątki",
"3 threads": "3 wątki",
"4 threads": "4 wątki",
"5 threads": "5 wątków",
"10 threads": "10 wątków",
" configuration item value changed": " obiekt konfiguracji zmieniony",
"Save settings": "Zapisz ustawienia",
"Some changed settings requires restart": "Niektóre zmiany ustawień wymagają ponownego uruchomienia ZeroNet",
"Restart ZeroNet client": "Uruchom ponownie ZeroNet"
}

View File

@ -0,0 +1,56 @@
{
"ZeroNet config": "Configurações da ZeroNet",
"Web Interface": "Interface Web",
"Open web browser on ZeroNet startup": "Abrir navegador da web quando a ZeroNet iniciar",
"Network": "Rede",
"File server port": "Porta de servidor de arquivos",
"Other peers will use this port to reach your served sites. (default: 15441)": "Outros peers usarão esta porta para alcançar seus sites servidos. (padrão: 15441)",
"Disable: Don't connect to peers on Tor network": "Desativar: Não conectar à peers na rede Tor",
"Enable: Only use Tor for Tor network peers": "Ativar: Somente usar Tor para os peers da rede Tor",
"Always: Use Tor for every connections to hide your IP address (slower)": "Sempre: Usar o Tor para todas as conexões, para esconder seu endereço IP (mais lento)",
"Disable": "Desativar",
"Enable": "Ativar",
"Always": "Sempre",
"Use Tor bridges": "Usar pontes do Tor",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Usar relays de ponte ofuscados para evitar o bloqueio de Tor de nível de rede (ainda mais lento)",
"Discover new peers using these adresses": "Descobrir novos peers usando estes endereços",
"Trackers files": "Arquivos de trackers",
"Load additional list of torrent trackers dynamically, from a file": "Carregar lista adicional de trackers de torrent dinamicamente, à partir de um arquivo",
"Eg.: data/trackers.json": "Exemplo: data/trackers.json",
"Proxy for tracker connections": "Proxy para conexões de tracker",
"configuration item value changed": " valor do item de configuração foi alterado",
"Save settings": "Salvar configurações",
"Some changed settings requires restart": "Algumas configurações alteradas requrem reinicialização",
"Restart ZeroNet client": "Reiniciar cliente da ZeroNet",
"Offline mode": "Modo offline",
"Disable network communication.": "Desativar a comunicação em rede.",
"File server network": "Rede do servidor de arquivos",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Aceite pontos de entrada usando o endereço IPv4 ou IPv6. (padrão: dual)",
"File server external ip": "IP externo do servidor de arquivos",
"Performance": "Desempenho",
"Level of logging to file": "Nível de registro no arquivo",
"Everything": "Tudo",
"Only important messages": "Apenas mensagens importantes",
"Only errors": "Apenas erros",
"Threads for async file system reads": "Threads para leituras de sistema de arquivos assíncronas",
"Threads for async file system writes": "Threads para gravações do sistema de arquivos assíncrono",
"Threads for cryptographic functions": "Threads para funções criptográficas",
"Threads for database operations": "Threads para operações de banco de dados",
"Sync execution": "Execução de sincronização",
"Custom": "Personalizado",
"Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers",
"Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)",
"Detect automatically": "Detectar automaticamente",
" configuration item value changed": " valor do item de configuração alterado"
}

View File

@ -0,0 +1,62 @@
{
"ZeroNet config": "ZeroNet配置",
"Web Interface": "网页界面",
"Open web browser on ZeroNet startup": "ZeroNet启动时,打开浏览器",
"Network": "网络",
"Offline mode": "离线模式",
"Disable network communication.": "关闭网络通信.",
"File server network": "文件服务器网络",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "使用IPv4或IPv6地址接受传入的节点请求. (默认:双重)",
"Dual (IPv4 & IPv6)": "双重 (IPv4与IPv6)",
"File server port": "文件服务器端口",
"Other peers will use this port to reach your served sites. (default: 15441)": "其他节点可通过该端口访问您服务的站点. (默认:15441)",
"File server external ip": "文件服务器外部IP",
"Detect automatically": "自动检测",
"Your file server is accessible on these ips. (default: detect automatically)": "其他节点可通过这些IP访问您的文件服务器. (默认:自动检测)",
"Disable: Don't connect to peers on Tor network": "关闭: 不连接洋葱网络中的节点",
"Enable: Only use Tor for Tor network peers": "开启: Tor仅用于连接洋葱网络中的节点",
"Always: Use Tor for every connections to hide your IP address (slower)": "总是: 将Tor用于每个网络连接,从而隐藏您的IP地址 (很慢)",
"Disable": "关闭",
"Enable": "开启",
"Always": "总是",
"Use Tor bridges": "使用Tor网桥",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "使用混淆网桥中继,从而避免网络层Tor阻碍 (超级慢)",
"Discover new peers using these adresses": "使用这些地址发现新节点",
"Trackers files": "Trackers文件",
"Load additional list of torrent trackers dynamically, from a file": "从一个文件中,动态加载额外的种子Trackers列表",
"Eg.: data/trackers.json": "例如: data/trackers.json",
"Proxy for tracker connections": "Tracker连接代理",
"Custom": "自定义",
"Custom socks proxy address for trackers": "Tracker的自定义socks代理地址",
"Performance": "性能",
"Level of logging to file": "日志记录级别",
"Everything": "记录全部",
"Only important messages": "仅记录重要信息",
"Only errors": "仅记录错误",
"Threads for async file system reads": "文件系统异步读取线程",
"Threads for async file system writes": "文件系统异步写入线程",
"Threads for cryptographic functions": "密码函数线程",
"Threads for database operations": "数据库操作线程",
"Sync read": "同步读取",
"Sync write": "同步写入",
"Sync execution": "同步执行",
"1 thread": "1个线程",
"2 threads": "2个线程",
"3 threads": "3个线程",
"4 threads": "4个线程",
"5 threads": "5个线程",
"10 threads": "10个线程",
" configuration item value changed": " 个配置值已经改变",
"Save settings": "保存配置",
"Some changed settings requires restart": "一些配置已改变,需要重启才能生效",
"Restart ZeroNet client": "重启ZeroNet客户端"
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Settings - ZeroNet</title>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="css/all.css?rev={rev}" />
</head>
<h1>ZeroNet config</h1>
<div class="content" id="content"></div>
<div class="bottom" id="bottom-save"></div>
<div class="bottom" id="bottom-restart"></div>
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
</body>
</html>

View File

@ -0,0 +1,68 @@
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
h2 { margin-top: 10px; }
h3 { font-weight: normal }
a { color: #9760F9 }
a:hover { text-decoration: none }
.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none }
.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; }
.section { margin: 0px 10%; }
.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
.config-item { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
.config-item.hidden { opacity: 0; height: 0px; padding: 0px; }
.config-item .title { display: inline-block; line-height: 36px; }
.config-item .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
.config-item .description { font-size: 14px; color: #666; line-height: 24px; }
.config-item .value { display: inline-block; white-space: nowrap; }
.config-item .value-right { right: 0px; position: absolute; }
.config-item .value-fullwidth { width: 100% }
.config-item .marker {
font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px;
opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9;
}
.config-item .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
.config-item .marker.changed { color: #2ecc71; }
.config-item .marker.pending { color: #ffa200; }
.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; }
.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; }
.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; }
.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; }
.value-right .input-text { text-align: right; width: 100px; }
.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; }
.value-fullwidth { margin-top: 10px; }
/* Checkbox */
.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; }
.checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; }
.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px }
.checkbox.checked .checkbox-skin:before { margin-left: 27px; }
.checkbox.checked .checkbox-skin { background-color: #2ECC71 }
/* Bottom */
.bottom {
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box;
}
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
.bottom .button { float: right; }
.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; }
.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; }
.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; }
.bottom-restart .title:before { color: #ffa200; }
.animate { transition: all 0.3s ease-out !important; }
.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
/* Button */
.button {
background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center;
border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none;
}
.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 }
.button:active { position: relative; top: 1px }
.button.loading {
color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center;
transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666
}
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View File

@ -0,0 +1,222 @@
class ConfigStorage extends Class
constructor: (@config) ->
@items = []
@createSections()
@setValues(@config)
setValues: (values) ->
for section in @items
for item in section.items
if not values[item.key]
continue
item.value = @formatValue(values[item.key].value)
item.default = @formatValue(values[item.key].default)
item.pending = values[item.key].pending
values[item.key].item = item
formatValue: (value) ->
if not value
return false
else if typeof(value) == "object"
return value.join("\n")
else if typeof(value) == "number"
return value.toString()
else
return value
deformatValue: (value, type) ->
if type == "object" and typeof(value) == "string"
if not value.length
return value = null
else
return value.split("\n")
if type == "boolean" and not value
return false
else if type == "number"
if typeof(value) == "number"
return value.toString()
else if not value
return "0"
else
return value
else
return value
createSections: ->
# Web Interface
section = @createSection("Web Interface")
section.items.push
key: "open_browser"
title: "Open web browser on ZeroNet startup"
type: "checkbox"
# Network
section = @createSection("Network")
section.items.push
key: "offline"
title: "Offline mode"
type: "checkbox"
description: "Disable network communication."
section.items.push
key: "fileserver_ip_type"
title: "File server network"
type: "select"
options: [
{title: "IPv4", value: "ipv4"}
{title: "IPv6", value: "ipv6"}
{title: "Dual (IPv4 & IPv6)", value: "dual"}
]
description: "Accept incoming peers using IPv4 or IPv6 address. (default: dual)"
section.items.push
key: "fileserver_port"
title: "File server port"
type: "text"
valid_pattern: /[0-9]*/
description: "Other peers will use this port to reach your served sites. (default: randomize)"
section.items.push
key: "ip_external"
title: "File server external ip"
type: "textarea"
placeholder: "Detect automatically"
description: "Your file server is accessible on these ips. (default: detect automatically)"
section.items.push
title: "Tor"
key: "tor"
type: "select"
options: [
{title: "Disable", value: "disable"}
{title: "Enable", value: "enable"}
{title: "Always", value: "always"}
]
description: [
"Disable: Don't connect to peers on Tor network", h("br"),
"Enable: Only use Tor for Tor network peers", h("br"),
"Always: Use Tor for every connections to hide your IP address (slower)"
]
section.items.push
title: "Use Tor bridges"
key: "tor_use_bridges"
type: "checkbox"
description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
isHidden: ->
return not Page.server_info.tor_has_meek_bridges
section.items.push
title: "Trackers"
key: "trackers"
type: "textarea"
description: "Discover new peers using these adresses"
section.items.push
title: "Trackers files"
key: "trackers_file"
type: "textarea"
description: "Load additional list of torrent trackers dynamically, from a file"
placeholder: "Eg.: {data_dir}/trackers.json"
value_pos: "fullwidth"
section.items.push
title: "Proxy for tracker connections"
key: "trackers_proxy"
type: "select"
options: [
{title: "Custom", value: ""}
{title: "Tor", value: "tor"}
{title: "Disable", value: "disable"}
]
isHidden: ->
Page.values["tor"] == "always"
section.items.push
title: "Custom socks proxy address for trackers"
key: "trackers_proxy"
type: "text"
placeholder: "Eg.: 127.0.0.1:1080"
value_pos: "fullwidth"
valid_pattern: /.+:[0-9]+/
isHidden: =>
Page.values["trackers_proxy"] in ["tor", "disable"]
# Performance
section = @createSection("Performance")
section.items.push
key: "log_level"
title: "Level of logging to file"
type: "select"
options: [
{title: "Everything", value: "DEBUG"}
{title: "Only important messages", value: "INFO"}
{title: "Only errors", value: "ERROR"}
]
section.items.push
key: "threads_fs_read"
title: "Threads for async file system reads"
type: "select"
options: [
{title: "Sync read", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_fs_write"
title: "Threads for async file system writes"
type: "select"
options: [
{title: "Sync write", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_crypt"
title: "Threads for cryptographic functions"
type: "select"
options: [
{title: "Sync execution", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_db"
title: "Threads for database operations"
type: "select"
options: [
{title: "Sync execution", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
createSection: (title) =>
section = {}
section.title = title
section.items = []
@items.push(section)
return section
window.ConfigStorage = ConfigStorage

View File

@ -0,0 +1,124 @@
class ConfigView extends Class
constructor: () ->
@
render: ->
@config_storage.items.map @renderSection
renderSection: (section) =>
h("div.section", {key: section.title}, [
h("h2", section.title),
h("div.config-items", section.items.map @renderSectionItem)
])
handleResetClick: (e) =>
node = e.currentTarget
config_key = node.attributes.config_key.value
default_value = node.attributes.default_value?.value
Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) =>
if (res)
@values[config_key] = default_value
Page.projector.scheduleRender()
renderSectionItem: (item) =>
value_pos = item.value_pos
if item.type == "textarea"
value_pos ?= "fullwidth"
else
value_pos ?= "right"
value_changed = @config_storage.formatValue(@values[item.key]) != item.value
value_default = @config_storage.formatValue(@values[item.key]) == item.default
if item.key in ["open_browser", "fileserver_port"] # Value default for some settings makes no sense
value_default = true
marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}"
if item.pending
marker_title += " (change pending until client restart)"
if item.isHidden?()
return null
h("div.config-item", {key: item.title, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUpInout}, [
h("div.title", [
h("h3", item.title),
h("div.description", item.description)
])
h("div.value.value-#{value_pos}",
if item.type == "select"
@renderValueSelect(item)
else if item.type == "checkbox"
@renderValueCheckbox(item)
else if item.type == "textarea"
@renderValueTextarea(item)
else
@renderValueText(item)
h("a.marker", {
href: "#Reset", title: marker_title,
onclick: @handleResetClick, config_key: item.key, default_value: item.default,
classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending}
}, "\u2022")
)
])
# Values
handleInputChange: (e) =>
node = e.target
config_key = node.attributes.config_key.value
@values[config_key] = node.value
Page.projector.scheduleRender()
handleCheckboxChange: (e) =>
node = e.currentTarget
config_key = node.attributes.config_key.value
value = not node.classList.contains("checked")
@values[config_key] = value
Page.projector.scheduleRender()
renderValueText: (item) =>
value = @values[item.key]
if not value
value = ""
h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange})
autosizeTextarea: (e) =>
if e.currentTarget
# @handleInputChange(e)
node = e.currentTarget
else
node = e
height_before = node.style.height
if height_before
node.style.height = "0px"
h = node.offsetHeight
scrollh = node.scrollHeight + 20
if scrollh > h
node.style.height = scrollh + "px"
else
node.style.height = height_before
renderValueTextarea: (item) =>
value = @values[item.key]
if not value
value = ""
h("textarea.input-#{item.type}.input-text",{
type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea,
updateAnimation: @autosizeTextarea, value: value, placeholder: item.placeholder
})
renderValueCheckbox: (item) =>
if @values[item.key] and @values[item.key] != "False"
checked = true
else
checked = false
h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin"))
renderValueSelect: (item) =>
h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
item.options.map (option) =>
h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
)
window.ConfigView = ConfigView

View File

@ -0,0 +1,129 @@
window.h = maquette.h
class UiConfig extends ZeroFrame
init: ->
@save_visible = true
@config = null # Setting currently set on the server
@values = null # Entered values on the page
@config_view = new ConfigView()
window.onbeforeunload = =>
if @getValuesChanged().length > 0
return true
else
return null
onOpenWebsocket: =>
@cmd("wrapperSetTitle", "Config - ZeroNet")
@cmd "serverInfo", {}, (server_info) =>
@server_info = server_info
@restart_loading = false
@updateConfig()
updateConfig: (cb) =>
@cmd "configList", [], (res) =>
@config = res
@values = {}
@config_storage = new ConfigStorage(@config)
@config_view.values = @values
@config_view.config_storage = @config_storage
for key, item of res
value = item.value
@values[key] = @config_storage.formatValue(value)
@projector.scheduleRender()
cb?()
createProjector: =>
@projector = maquette.createProjector()
@projector.replace($("#content"), @render)
@projector.replace($("#bottom-save"), @renderBottomSave)
@projector.replace($("#bottom-restart"), @renderBottomRestart)
getValuesChanged: =>
values_changed = []
for key, value of @values
if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value)
values_changed.push({key: key, value: value})
return values_changed
getValuesPending: =>
values_pending = []
for key, item of @config
if item.pending
values_pending.push(key)
return values_pending
saveValues: (cb) =>
changed_values = @getValuesChanged()
for item, i in changed_values
last = i == changed_values.length - 1
value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default))
default_value = @config_storage.deformatValue(@config[item.key].default, typeof(@config[item.key].default))
value_same_as_default = JSON.stringify(default_value) == JSON.stringify(value)
if @config[item.key].item.valid_pattern and not @config[item.key].item.isHidden?()
match = value.match(@config[item.key].item.valid_pattern)
if not match or match[0] != value
message = "Invalid value of #{@config[item.key].item.title}: #{value} (does not matches #{@config[item.key].item.valid_pattern})"
Page.cmd("wrapperNotification", ["error", message])
cb(false)
break
if value_same_as_default
value = null
@saveValue(item.key, value, if last then cb else null)
saveValue: (key, value, cb) =>
if key == "open_browser"
if value
value = "default_browser"
else
value = "False"
Page.cmd "configSet", [key, value], (res) =>
if res != "ok"
Page.cmd "wrapperNotification", ["error", res.error]
cb?(true)
render: =>
if not @config
return h("div.content")
h("div.content", [
@config_view.render()
])
handleSaveClick: =>
@save_loading = true
@logStart "Save"
@saveValues (success) =>
@save_loading = false
@logEnd "Save"
if success
@updateConfig()
Page.projector.scheduleRender()
return false
renderBottomSave: =>
values_changed = @getValuesChanged()
h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [
h("div.title", "#{values_changed.length} configuration item value changed"),
h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings")
]))
handleRestartClick: =>
@restart_loading = true
Page.cmd("serverShutdown", {restart: true})
Page.projector.scheduleRender()
return false
renderBottomRestart: =>
values_pending = @getValuesPending()
values_changed = @getValuesChanged()
h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [
h("div.title", "Some changed settings requires restart"),
h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client")
]))
window.Page = new UiConfig()
window.Page.createProjector()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
class Class
trace: true
log: (args...) ->
return unless @trace
return if typeof console is 'undefined'
args.unshift("[#{@.constructor.name}]")
console.log(args...)
@
logStart: (name, args...) ->
return unless @trace
@logtimers or= {}
@logtimers[name] = +(new Date)
@log "#{name}", args..., "(started)" if args.length > 0
@
logEnd: (name, args...) ->
ms = +(new Date)-@logtimers[name]
@log "#{name}", args..., "(Done in #{ms}ms)"
@
window.Class = Class

View File

@ -0,0 +1,74 @@
# From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html
class Promise
@when: (tasks...) ->
num_uncompleted = tasks.length
args = new Array(num_uncompleted)
promise = new Promise()
for task, task_id in tasks
((task_id) ->
task.then(() ->
args[task_id] = Array.prototype.slice.call(arguments)
num_uncompleted--
promise.complete.apply(promise, args) if num_uncompleted == 0
)
)(task_id)
return promise
constructor: ->
@resolved = false
@end_promise = null
@result = null
@callbacks = []
resolve: ->
if @resolved
return false
@resolved = true
@data = arguments
if not arguments.length
@data = [true]
@result = @data[0]
for callback in @callbacks
back = callback.apply callback, @data
if @end_promise
@end_promise.resolve(back)
fail: ->
@resolve(false)
then: (callback) ->
if @resolved == true
callback.apply callback, @data
return
@callbacks.push callback
@end_promise = new Promise()
window.Promise = Promise
###
s = Date.now()
log = (text) ->
console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
log "Started"
cmd = (query) ->
p = new Promise()
setTimeout ( ->
p.resolve query+" Result"
), 100
return p
back = cmd("SELECT * FROM message").then (res) ->
log res
return "Return from query"
.then (res) ->
log "Back then", res
log "Query started", back
###

View File

@ -0,0 +1,8 @@
String::startsWith = (s) -> @[...s.length] is s
String::endsWith = (s) -> s is '' or @[-s.length..] is s
String::repeat = (count) -> new Array( count + 1 ).join(@)
window.isEmpty = (obj) ->
for key of obj
return false
return true

View File

@ -0,0 +1,770 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports);
} else {
// Browser globals
factory(root.maquette = {});
}
}(this, function (exports) {
'use strict';
;
;
;
;
var NAMESPACE_W3 = 'http://www.w3.org/';
var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
// Utilities
var emptyArray = [];
var extend = function (base, overrides) {
var result = {};
Object.keys(base).forEach(function (key) {
result[key] = base[key];
});
if (overrides) {
Object.keys(overrides).forEach(function (key) {
result[key] = overrides[key];
});
}
return result;
};
// Hyperscript helper functions
var same = function (vnode1, vnode2) {
if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
return false;
}
if (vnode1.properties && vnode2.properties) {
if (vnode1.properties.key !== vnode2.properties.key) {
return false;
}
return vnode1.properties.bind === vnode2.properties.bind;
}
return !vnode1.properties && !vnode2.properties;
};
var toTextVNode = function (data) {
return {
vnodeSelector: '',
properties: undefined,
children: undefined,
text: data.toString(),
domNode: null
};
};
var appendChildren = function (parentSelector, insertions, main) {
for (var i = 0; i < insertions.length; i++) {
var item = insertions[i];
if (Array.isArray(item)) {
appendChildren(parentSelector, item, main);
} else {
if (item !== null && item !== undefined) {
if (!item.hasOwnProperty('vnodeSelector')) {
item = toTextVNode(item);
}
main.push(item);
}
}
}
};
// Render helper functions
var missingTransition = function () {
throw new Error('Provide a transitions object to the projectionOptions to do animations');
};
var DEFAULT_PROJECTION_OPTIONS = {
namespace: undefined,
eventHandlerInterceptor: undefined,
styleApplyer: function (domNode, styleName, value) {
// Provides a hook to add vendor prefixes for browsers that still need it.
domNode.style[styleName] = value;
},
transitions: {
enter: missingTransition,
exit: missingTransition
}
};
var applyDefaultProjectionOptions = function (projectorOptions) {
return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
};
var checkStyleValue = function (styleValue) {
if (typeof styleValue !== 'string') {
throw new Error('Style values must be strings');
}
};
var setProperties = function (domNode, properties, projectionOptions) {
if (!properties) {
return;
}
var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
var propNames = Object.keys(properties);
var propCount = propNames.length;
for (var i = 0; i < propCount; i++) {
var propName = propNames[i];
/* tslint:disable:no-var-keyword: edge case */
var propValue = properties[propName];
/* tslint:enable:no-var-keyword */
if (propName === 'className') {
throw new Error('Property "className" is not supported, use "class".');
} else if (propName === 'class') {
if (domNode.className) {
// May happen if classes is specified before class
domNode.className += ' ' + propValue;
} else {
domNode.className = propValue;
}
} else if (propName === 'classes') {
// object with string keys and boolean values
var classNames = Object.keys(propValue);
var classNameCount = classNames.length;
for (var j = 0; j < classNameCount; j++) {
var className = classNames[j];
if (propValue[className]) {
domNode.classList.add(className);
}
}
} else if (propName === 'styles') {
// object with string keys and string (!) values
var styleNames = Object.keys(propValue);
var styleCount = styleNames.length;
for (var j = 0; j < styleCount; j++) {
var styleName = styleNames[j];
var styleValue = propValue[styleName];
if (styleValue) {
checkStyleValue(styleValue);
projectionOptions.styleApplyer(domNode, styleName, styleValue);
}
}
} else if (propName === 'key') {
continue;
} else if (propValue === null || propValue === undefined) {
continue;
} else {
var type = typeof propValue;
if (type === 'function') {
if (propName.lastIndexOf('on', 0) === 0) {
if (eventHandlerInterceptor) {
propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
}
if (propName === 'oninput') {
(function () {
// record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
var oldPropValue = propValue;
propValue = function (evt) {
evt.target['oninput-value'] = evt.target.value;
// may be HTMLTextAreaElement as well
oldPropValue.apply(this, [evt]);
};
}());
}
domNode[propName] = propValue;
}
} else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
} else {
domNode.setAttribute(propName, propValue);
}
} else {
domNode[propName] = propValue;
}
}
}
};
var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
if (!properties) {
return;
}
var propertiesUpdated = false;
var propNames = Object.keys(properties);
var propCount = propNames.length;
for (var i = 0; i < propCount; i++) {
var propName = propNames[i];
// assuming that properties will be nullified instead of missing is by design
var propValue = properties[propName];
var previousValue = previousProperties[propName];
if (propName === 'class') {
if (previousValue !== propValue) {
throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
}
} else if (propName === 'classes') {
var classList = domNode.classList;
var classNames = Object.keys(propValue);
var classNameCount = classNames.length;
for (var j = 0; j < classNameCount; j++) {
var className = classNames[j];
var on = !!propValue[className];
var previousOn = !!previousValue[className];
if (on === previousOn) {
continue;
}
propertiesUpdated = true;
if (on) {
classList.add(className);
} else {
classList.remove(className);
}
}
} else if (propName === 'styles') {
var styleNames = Object.keys(propValue);
var styleCount = styleNames.length;
for (var j = 0; j < styleCount; j++) {
var styleName = styleNames[j];
var newStyleValue = propValue[styleName];
var oldStyleValue = previousValue[styleName];
if (newStyleValue === oldStyleValue) {
continue;
}
propertiesUpdated = true;
if (newStyleValue) {
checkStyleValue(newStyleValue);
projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
} else {
projectionOptions.styleApplyer(domNode, styleName, '');
}
}
} else {
if (!propValue && typeof previousValue === 'string') {
propValue = '';
}
if (propName === 'value') {
if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
domNode[propName] = propValue;
// Reset the value, even if the virtual DOM did not change
domNode['oninput-value'] = undefined;
}
// else do not update the domNode, otherwise the cursor position would be changed
if (propValue !== previousValue) {
propertiesUpdated = true;
}
} else if (propValue !== previousValue) {
var type = typeof propValue;
if (type === 'function') {
throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
}
if (type === 'string' && propName !== 'innerHTML') {
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
} else {
domNode.setAttribute(propName, propValue);
}
} else {
if (domNode[propName] !== propValue) {
domNode[propName] = propValue;
}
}
propertiesUpdated = true;
}
}
}
return propertiesUpdated;
};
var findIndexOfChild = function (children, sameAs, start) {
if (sameAs.vnodeSelector !== '') {
// Never scan for text-nodes
for (var i = start; i < children.length; i++) {
if (same(children[i], sameAs)) {
return i;
}
}
}
return -1;
};
var nodeAdded = function (vNode, transitions) {
if (vNode.properties) {
var enterAnimation = vNode.properties.enterAnimation;
if (enterAnimation) {
if (typeof enterAnimation === 'function') {
enterAnimation(vNode.domNode, vNode.properties);
} else {
transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
}
}
}
};
var nodeToRemove = function (vNode, transitions) {
var domNode = vNode.domNode;
if (vNode.properties) {
var exitAnimation = vNode.properties.exitAnimation;
if (exitAnimation) {
domNode.style.pointerEvents = 'none';
var removeDomNode = function () {
if (domNode.parentNode) {
domNode.parentNode.removeChild(domNode);
}
};
if (typeof exitAnimation === 'function') {
exitAnimation(domNode, removeDomNode, vNode.properties);
return;
} else {
transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
return;
}
}
}
if (domNode.parentNode) {
domNode.parentNode.removeChild(domNode);
}
};
var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
var childNode = childNodes[indexToCheck];
if (childNode.vnodeSelector === '') {
return; // Text nodes need not be distinguishable
}
var properties = childNode.properties;
var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
if (!key) {
for (var i = 0; i < childNodes.length; i++) {
if (i !== indexToCheck) {
var node = childNodes[i];
if (same(node, childNode)) {
if (operation === 'added') {
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
} else {
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
}
}
}
}
}
};
var createDom;
var updateDom;
var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
if (oldChildren === newChildren) {
return false;
}
oldChildren = oldChildren || emptyArray;
newChildren = newChildren || emptyArray;
var oldChildrenLength = oldChildren.length;
var newChildrenLength = newChildren.length;
var transitions = projectionOptions.transitions;
var oldIndex = 0;
var newIndex = 0;
var i;
var textUpdated = false;
while (newIndex < newChildrenLength) {
var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
var newChild = newChildren[newIndex];
if (oldChild !== undefined && same(oldChild, newChild)) {
textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
oldIndex++;
} else {
var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
if (findOldIndex >= 0) {
// Remove preceding missing children
for (i = oldIndex; i < findOldIndex; i++) {
nodeToRemove(oldChildren[i], transitions);
checkDistinguishable(oldChildren, i, vnode, 'removed');
}
textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
oldIndex = findOldIndex + 1;
} else {
// New child
createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
nodeAdded(newChild, transitions);
checkDistinguishable(newChildren, newIndex, vnode, 'added');
}
}
newIndex++;
}
if (oldChildrenLength > oldIndex) {
// Remove child fragments
for (i = oldIndex; i < oldChildrenLength; i++) {
nodeToRemove(oldChildren[i], transitions);
checkDistinguishable(oldChildren, i, vnode, 'removed');
}
}
return textUpdated;
};
var addChildren = function (domNode, children, projectionOptions) {
if (!children) {
return;
}
for (var i = 0; i < children.length; i++) {
createDom(children[i], domNode, undefined, projectionOptions);
}
};
var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
addChildren(domNode, vnode.children, projectionOptions);
// children before properties, needed for value property of <select>.
if (vnode.text) {
domNode.textContent = vnode.text;
}
setProperties(domNode, vnode.properties, projectionOptions);
if (vnode.properties && vnode.properties.afterCreate) {
vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
}
};
createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
var domNode, i, c, start = 0, type, found;
var vnodeSelector = vnode.vnodeSelector;
if (vnodeSelector === '') {
domNode = vnode.domNode = document.createTextNode(vnode.text);
if (insertBefore !== undefined) {
parentNode.insertBefore(domNode, insertBefore);
} else {
parentNode.appendChild(domNode);
}
} else {
for (i = 0; i <= vnodeSelector.length; ++i) {
c = vnodeSelector.charAt(i);
if (i === vnodeSelector.length || c === '.' || c === '#') {
type = vnodeSelector.charAt(start - 1);
found = vnodeSelector.slice(start, i);
if (type === '.') {
domNode.classList.add(found);
} else if (type === '#') {
domNode.id = found;
} else {
if (found === 'svg') {
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
}
if (projectionOptions.namespace !== undefined) {
domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
} else {
domNode = vnode.domNode = document.createElement(found);
}
if (insertBefore !== undefined) {
parentNode.insertBefore(domNode, insertBefore);
} else {
parentNode.appendChild(domNode);
}
}
start = i + 1;
}
}
initPropertiesAndChildren(domNode, vnode, projectionOptions);
}
};
updateDom = function (previous, vnode, projectionOptions) {
var domNode = previous.domNode;
var textUpdated = false;
if (previous === vnode) {
return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
}
var updated = false;
if (vnode.vnodeSelector === '') {
if (vnode.text !== previous.text) {
var newVNode = document.createTextNode(vnode.text);
domNode.parentNode.replaceChild(newVNode, domNode);
vnode.domNode = newVNode;
textUpdated = true;
return textUpdated;
}
} else {
if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
}
if (previous.text !== vnode.text) {
updated = true;
if (vnode.text === undefined) {
domNode.removeChild(domNode.firstChild); // the only textnode presumably
} else {
domNode.textContent = vnode.text;
}
}
updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
if (vnode.properties && vnode.properties.afterUpdate) {
vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
}
}
if (updated && vnode.properties && vnode.properties.updateAnimation) {
vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
}
vnode.domNode = previous.domNode;
return textUpdated;
};
var createProjection = function (vnode, projectionOptions) {
return {
update: function (updatedVnode) {
if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
}
updateDom(vnode, updatedVnode, projectionOptions);
vnode = updatedVnode;
},
domNode: vnode.domNode
};
};
;
// The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
exports.h = function (selector) {
var properties = arguments[1];
if (typeof selector !== 'string') {
throw new Error();
}
var childIndex = 1;
if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
childIndex = 2;
} else {
// Optional properties argument was omitted
properties = undefined;
}
var text = undefined;
var children = undefined;
var argsLength = arguments.length;
// Recognize a common special case where there is only a single text node
if (argsLength === childIndex + 1) {
var onlyChild = arguments[childIndex];
if (typeof onlyChild === 'string') {
text = onlyChild;
} else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
text = onlyChild[0];
}
}
if (text === undefined) {
children = [];
for (; childIndex < arguments.length; childIndex++) {
var child = arguments[childIndex];
if (child === null || child === undefined) {
continue;
} else if (Array.isArray(child)) {
appendChildren(selector, child, children);
} else if (child.hasOwnProperty('vnodeSelector')) {
children.push(child);
} else {
children.push(toTextVNode(child));
}
}
}
return {
vnodeSelector: selector,
properties: properties,
children: children,
text: text === '' ? undefined : text,
domNode: null
};
};
/**
* Contains simple low-level utility functions to manipulate the real DOM.
*/
exports.dom = {
/**
* Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
* its [[Projection.domNode|domNode]] property.
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
* objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection.
* @returns The [[Projection]] which also contains the DOM Node that was created.
*/
create: function (vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, document.createElement('div'), undefined, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Appends a new childnode to the DOM which is generated from a [[VNode]].
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param parentNode - The parent node for the new childNode.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
* objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the [[Projection]].
* @returns The [[Projection]] that was created.
*/
append: function (parentNode, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, parentNode, undefined, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Inserts a new DOM node which is generated from a [[VNode]].
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param beforeNode - The node that the DOM Node is inserted before.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
* NOTE: [[VNode]] objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
* @returns The [[Projection]] that was created.
*/
insertBefore: function (beforeNode, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
* This means that the virtual DOM and the real DOM will have one overlapping element.
* Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
* may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
* @returns The [[Projection]] that was created.
*/
merge: function (element, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
vnode.domNode = element;
initPropertiesAndChildren(element, vnode, projectionOptions);
return createProjection(vnode, projectionOptions);
}
};
/**
* Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
* In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
* For more information, see [[CalculationCache]].
*
* @param <Result> The type of the value that is cached.
*/
exports.createCache = function () {
var cachedInputs = undefined;
var cachedOutcome = undefined;
var result = {
invalidate: function () {
cachedOutcome = undefined;
cachedInputs = undefined;
},
result: function (inputs, calculation) {
if (cachedInputs) {
for (var i = 0; i < inputs.length; i++) {
if (cachedInputs[i] !== inputs[i]) {
cachedOutcome = undefined;
}
}
}
if (!cachedOutcome) {
cachedOutcome = calculation();
cachedInputs = inputs;
}
return cachedOutcome;
}
};
return result;
};
/**
* Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
* See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
*
* @param <Source> The type of source items. A database-record for instance.
* @param <Target> The type of target items. A [[Component]] for instance.
* @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
* @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
* to the `callback` argument in `Array.map(callback)`.
* @param updateResult `function(source, target, index)` that updates a result to an updated source.
*/
exports.createMapping = function (getSourceKey, createResult, updateResult) {
var keys = [];
var results = [];
return {
results: results,
map: function (newSources) {
var newKeys = newSources.map(getSourceKey);
var oldTargets = results.slice();
var oldIndex = 0;
for (var i = 0; i < newSources.length; i++) {
var source = newSources[i];
var sourceKey = newKeys[i];
if (sourceKey === keys[oldIndex]) {
results[i] = oldTargets[oldIndex];
updateResult(source, oldTargets[oldIndex], i);
oldIndex++;
} else {
var found = false;
for (var j = 1; j < keys.length; j++) {
var searchIndex = (oldIndex + j) % keys.length;
if (keys[searchIndex] === sourceKey) {
results[i] = oldTargets[searchIndex];
updateResult(newSources[i], oldTargets[searchIndex], i);
oldIndex = searchIndex + 1;
found = true;
break;
}
}
if (!found) {
results[i] = createResult(source, i);
}
}
}
results.length = newSources.length;
keys = newKeys;
}
};
};
/**
* Creates a [[Projector]] instance using the provided projectionOptions.
*
* For more information, see [[Projector]].
*
* @param projectionOptions Options that influence how the DOM is rendered and updated.
*/
exports.createProjector = function (projectorOptions) {
var projector;
var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
return function () {
// intercept function calls (event handlers) to do a render afterwards.
projector.scheduleRender();
return eventHandler.apply(properties.bind || this, arguments);
};
};
var renderCompleted = true;
var scheduled;
var stopped = false;
var projections = [];
var renderFunctions = [];
// matches the projections array
var doRender = function () {
scheduled = undefined;
if (!renderCompleted) {
return; // The last render threw an error, it should be logged in the browser console.
}
renderCompleted = false;
for (var i = 0; i < projections.length; i++) {
var updatedVnode = renderFunctions[i]();
projections[i].update(updatedVnode);
}
renderCompleted = true;
};
projector = {
scheduleRender: function () {
if (!scheduled && !stopped) {
scheduled = requestAnimationFrame(doRender);
}
},
stop: function () {
if (scheduled) {
cancelAnimationFrame(scheduled);
scheduled = undefined;
}
stopped = true;
},
resume: function () {
stopped = false;
renderCompleted = true;
projector.scheduleRender();
},
append: function (parentNode, renderMaquetteFunction) {
projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
insertBefore: function (beforeNode, renderMaquetteFunction) {
projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
merge: function (domNode, renderMaquetteFunction) {
projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
replace: function (domNode, renderMaquetteFunction) {
var vnode = renderMaquetteFunction();
createDom(vnode, domNode.parentNode, domNode, projectionOptions);
domNode.parentNode.removeChild(domNode);
projections.push(createProjection(vnode, projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
detach: function (renderMaquetteFunction) {
for (var i = 0; i < renderFunctions.length; i++) {
if (renderFunctions[i] === renderMaquetteFunction) {
renderFunctions.splice(i, 1);
return projections.splice(i, 1)[0];
}
}
throw new Error('renderMaquetteFunction was not found');
}
};
return projector;
};
}));

View File

@ -0,0 +1,138 @@
class Animation
slideDown: (elem, props) ->
if elem.offsetTop > 2000
return
h = elem.offsetHeight
cstyle = window.getComputedStyle(elem)
margin_top = cstyle.marginTop
margin_bottom = cstyle.marginBottom
padding_top = cstyle.paddingTop
padding_bottom = cstyle.paddingBottom
transition = cstyle.transition
elem.style.boxSizing = "border-box"
elem.style.overflow = "hidden"
elem.style.transform = "scale(0.6)"
elem.style.opacity = "0"
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transition = "none"
setTimeout (->
elem.className += " animate-inout"
elem.style.height = h+"px"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.marginTop = margin_top
elem.style.marginBottom = margin_bottom
elem.style.paddingTop = padding_top
elem.style.paddingBottom = padding_bottom
), 1
elem.addEventListener "transitionend", ->
elem.classList.remove("animate-inout")
elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null
elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null
elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null
elem.removeEventListener "transitionend", arguments.callee, false
slideUp: (elem, remove_func, props) ->
if elem.offsetTop > 1000
return remove_func()
elem.className += " animate-back"
elem.style.boxSizing = "border-box"
elem.style.height = elem.offsetHeight+"px"
elem.style.overflow = "hidden"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.pointerEvents = "none"
setTimeout (->
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transform = "scale(0.8)"
elem.style.borderTopWidth = "0px"
elem.style.borderBottomWidth = "0px"
elem.style.opacity = "0"
), 1
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
elem.removeEventListener "transitionend", arguments.callee, false
remove_func()
slideUpInout: (elem, remove_func, props) ->
elem.className += " animate-inout"
elem.style.boxSizing = "border-box"
elem.style.height = elem.offsetHeight+"px"
elem.style.overflow = "hidden"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.pointerEvents = "none"
setTimeout (->
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transform = "scale(0.8)"
elem.style.borderTopWidth = "0px"
elem.style.borderBottomWidth = "0px"
elem.style.opacity = "0"
), 1
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
elem.removeEventListener "transitionend", arguments.callee, false
remove_func()
showRight: (elem, props) ->
elem.className += " animate"
elem.style.opacity = 0
elem.style.transform = "TranslateX(-20px) Scale(1.01)"
setTimeout (->
elem.style.opacity = 1
elem.style.transform = "TranslateX(0px) Scale(1)"
), 1
elem.addEventListener "transitionend", ->
elem.classList.remove("animate")
elem.style.transform = elem.style.opacity = null
show: (elem, props) ->
delay = arguments[arguments.length-2]?.delay*1000 or 1
elem.style.opacity = 0
setTimeout (->
elem.className += " animate"
), 1
setTimeout (->
elem.style.opacity = 1
), delay
elem.addEventListener "transitionend", ->
elem.classList.remove("animate")
elem.style.opacity = null
elem.removeEventListener "transitionend", arguments.callee, false
hide: (elem, remove_func, props) ->
delay = arguments[arguments.length-2]?.delay*1000 or 1
elem.className += " animate"
setTimeout (->
elem.style.opacity = 0
), delay
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity"
remove_func()
addVisibleClass: (elem, props) ->
setTimeout ->
elem.classList.add("visible")
window.Animation = new Animation()

View File

@ -0,0 +1,3 @@
window.$ = (selector) ->
if selector.startsWith("#")
return document.getElementById(selector.replace("#", ""))

View File

@ -0,0 +1,85 @@
class ZeroFrame extends Class
constructor: (url) ->
@url = url
@waiting_cb = {}
@wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1")
@connect()
@next_message_id = 1
@history_state = {}
@init()
init: ->
@
connect: ->
@target = window.parent
window.addEventListener("message", @onMessage, false)
@cmd("innerReady")
# Save scrollTop
window.addEventListener "beforeunload", (e) =>
@log "save scrollTop", window.pageYOffset
@history_state["scrollTop"] = window.pageYOffset
@cmd "wrapperReplaceState", [@history_state, null]
# Restore scrollTop
@cmd "wrapperGetState", [], (state) =>
@history_state = state if state?
@log "restore scrollTop", state, window.pageYOffset
if window.pageYOffset == 0 and state
window.scroll(window.pageXOffset, state.scrollTop)
onMessage: (e) =>
message = e.data
cmd = message.cmd
if cmd == "response"
if @waiting_cb[message.to]?
@waiting_cb[message.to](message.result)
else
@log "Websocket callback not found:", message
else if cmd == "wrapperReady" # Wrapper inited later
@cmd("innerReady")
else if cmd == "ping"
@response message.id, "pong"
else if cmd == "wrapperOpenedWebsocket"
@onOpenWebsocket()
else if cmd == "wrapperClosedWebsocket"
@onCloseWebsocket()
else
@onRequest cmd, message.params
onRequest: (cmd, message) =>
@log "Unknown request", message
response: (to, result) ->
@send {"cmd": "response", "to": to, "result": result}
cmd: (cmd, params={}, cb=null) ->
@send {"cmd": cmd, "params": params}, cb
send: (message, cb=null) ->
message.wrapper_nonce = @wrapper_nonce
message.id = @next_message_id
@next_message_id += 1
@target.postMessage(message, "*")
if cb
@waiting_cb[message.id] = cb
onOpenWebsocket: =>
@log "Websocket open"
onCloseWebsocket: =>
@log "Websocket close"
window.ZeroFrame = ZeroFrame

View File

@ -0,0 +1,5 @@
{
"name": "UiConfig",
"description": "Change client settings using the web interface.",
"default": "enabled"
}

View File

@ -0,0 +1,180 @@
import logging, json, os, re, sys, time, socket
from Plugin import PluginManager
from Config import config
from Debug import Debug
from http.client import HTTPSConnection, HTTPConnection, HTTPException
from base64 import b64encode
allow_reload = False # No reload supported
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
def load(self, *args, **kwargs):
super(SiteManagerPlugin, self).load(*args, **kwargs)
self.log = logging.getLogger("ZeronetLocal Plugin")
self.error_message = None
if not config.namecoin_host or not config.namecoin_rpcport or not config.namecoin_rpcuser or not config.namecoin_rpcpassword:
self.error_message = "Missing parameters"
self.log.error("Missing parameters to connect to namecoin node. Please check all the arguments needed with '--help'. Zeronet will continue working without it.")
return
url = "%(host)s:%(port)s" % {"host": config.namecoin_host, "port": config.namecoin_rpcport}
self.c = HTTPConnection(url, timeout=3)
user_pass = "%(user)s:%(password)s" % {"user": config.namecoin_rpcuser, "password": config.namecoin_rpcpassword}
userAndPass = b64encode(bytes(user_pass, "utf-8")).decode("ascii")
self.headers = {"Authorization" : "Basic %s" % userAndPass, "Content-Type": " application/json " }
payload = json.dumps({
"jsonrpc": "2.0",
"id": "zeronet",
"method": "ping",
"params": []
})
try:
self.c.request("POST", "/", payload, headers=self.headers)
response = self.c.getresponse()
data = response.read()
self.c.close()
if response.status == 200:
result = json.loads(data.decode())["result"]
else:
raise Exception(response.reason)
except Exception as err:
self.log.error("The Namecoin node is unreachable. Please check the configuration value are correct. Zeronet will continue working without it.")
self.error_message = err
self.cache = dict()
# Checks if it's a valid address
def isAddress(self, address):
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address)
# Return: True if the address is domain
def isDomain(self, address):
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
# Return: True if the address is .bit domain
def isBitDomain(self, address):
return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
# Return: Site object or None if not found
def get(self, address):
if self.isBitDomain(address): # Its looks like a domain
address_resolved = self.resolveDomain(address)
if address_resolved: # Domain found
site = self.sites.get(address_resolved)
if site:
site_domain = site.settings.get("domain")
if site_domain != address:
site.settings["domain"] = address
else: # Domain not found
site = self.sites.get(address)
else: # Access by site address
site = super(SiteManagerPlugin, self).get(address)
return site
# Return or create site and start download site files
# Return: Site or None if dns resolve failed
def need(self, address, *args, **kwargs):
if self.isBitDomain(address): # Its looks like a domain
address_resolved = self.resolveDomain(address)
if address_resolved:
address = address_resolved
else:
return None
return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
# Resolve domain
# Return: The address or None
def resolveDomain(self, domain):
domain = domain.lower()
#remove .bit on end
if domain[-4:] == ".bit":
domain = domain[0:-4]
domain_array = domain.split(".")
if self.error_message:
self.log.error("Not able to connect to Namecoin node : {!s}".format(self.error_message))
return None
if len(domain_array) > 2:
self.log.error("Too many subdomains! Can only handle one level (eg. staging.mixtape.bit)")
return None
subdomain = ""
if len(domain_array) == 1:
domain = domain_array[0]
else:
subdomain = domain_array[0]
domain = domain_array[1]
if domain in self.cache:
delta = time.time() - self.cache[domain]["time"]
if delta < 3600:
# Must have been less than 1hour
return self.cache[domain]["addresses_resolved"][subdomain]
payload = json.dumps({
"jsonrpc": "2.0",
"id": "zeronet",
"method": "name_show",
"params": ["d/"+domain]
})
try:
self.c.request("POST", "/", payload, headers=self.headers)
response = self.c.getresponse()
data = response.read()
self.c.close()
domain_object = json.loads(data.decode())["result"]
except Exception as err:
#domain doesn't exist
return None
if "zeronet" in domain_object["value"]:
zeronet_domains = json.loads(domain_object["value"])["zeronet"]
if isinstance(zeronet_domains, str):
# {
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
# } is valid
zeronet_domains = {"": zeronet_domains}
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
elif "map" in domain_object["value"]:
# Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
data_map = json.loads(domain_object["value"])["map"]
zeronet_domains = dict()
for subdomain in data_map:
if "zeronet" in data_map[subdomain]:
zeronet_domains[subdomain] = data_map[subdomain]["zeronet"]
if "zeronet" in data_map and isinstance(data_map["zeronet"], str):
# {"map":{
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
# }}
zeronet_domains[""] = data_map["zeronet"]
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
else:
# No Zeronet address registered
return None
return self.cache[domain]["addresses_resolved"][subdomain]
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("Zeroname Local plugin")
group.add_argument('--namecoin_host', help="Host to namecoin node (eg. 127.0.0.1)")
group.add_argument('--namecoin_rpcport', help="Port to connect (eg. 8336)")
group.add_argument('--namecoin_rpcuser', help="RPC user to connect to the namecoin node (eg. nofish)")
group.add_argument('--namecoin_rpcpassword', help="RPC password to connect to namecoin node")
return super(ConfigPlugin, self).createArguments()

View File

@ -0,0 +1,39 @@
import re
from Plugin import PluginManager
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def __init__(self, *args, **kwargs):
from Site import SiteManager
self.site_manager = SiteManager.site_manager
super(UiRequestPlugin, self).__init__(*args, **kwargs)
# Media request
def actionSiteMedia(self, path):
match = re.match(r"/media/(?P<address>[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", path)
if match: # Its a valid domain, resolve first
domain = match.group("address")
address = self.site_manager.resolveDomain(domain)
if address:
path = "/media/"+address+match.group("inner_path")
return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output
# Is mediarequest allowed from that referer
def isMediaRequestAllowed(self, site_address, referer):
referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
referer_path = re.sub(r"\?.*", "", referer_path) # Remove http params
if self.isProxyRequest(): # Match to site domain
referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access
referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1)
else: # Match to request path
referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
if referer_site_address == site_address: # Referer site address as simple address
return True
elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
return True
else: # Invalid referer
return False

View File

@ -0,0 +1,2 @@
from . import UiRequestPlugin
from . import SiteManagerPlugin