diff --git a/README.md b/README.md
index 069ab358..bd377827 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,7 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
* Unpack anywhere
* Run `ZeroNet.cmd` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
+* On OSX you may need to make the application executable via `chmod +x ZeroNet.app`
If you get "classic environment no longer supported" error on OS X: Open a Terminal window and drop ZeroNet.app on it
@@ -187,4 +188,4 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
-* Email: hello@noloop.me
+* Email: hello@zeronet.io (PGP: CB9613AE)
diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py
index 4440e718..3fb58cfd 100644
--- a/plugins/MergerSite/MergerSitePlugin.py
+++ b/plugins/MergerSite/MergerSitePlugin.py
@@ -5,6 +5,7 @@ from Plugin import PluginManager
from Translate import Translate
from util import RateLimit
from util import helper
+from Debug import Debug
try:
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
except Exception:
@@ -19,6 +20,7 @@ if "merger_db" not in locals().keys(): # To keep merger_sites between module re
if "_" not in locals():
_ = Translate("plugins/MergerSite/languages/")
+
# Check if the site has permission to this merger site
def checkMergerPath(address, inner_path):
merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
@@ -32,7 +34,10 @@ def checkMergerPath(address, inner_path):
inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
return merged_address, inner_path
else:
- raise Exception("Merger site (%s) does not have permission for merged site: %s" % (merger_type, merged_address))
+ raise Exception(
+ "Merger site (%s) does not have permission for merged site: %s (%s)" %
+ (merger_type, merged_address, merged_db.get(merged_address))
+ )
else:
raise Exception("No merger (%s) permission to load:
%s (%s not in %s)" % (
address, inner_path, merger_type, merger_db.get(address, []))
@@ -184,7 +189,8 @@ class UiWebsocketPlugin(object):
def actionPermissionAdd(self, to, permission):
super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
- self.site.storage.rebuildDb()
+ if permission.startswith("Merger"):
+ self.site.storage.rebuildDb()
@PluginManager.registerTo("UiRequest")
@@ -269,7 +275,6 @@ class SitePlugin(object):
for ws in merger_site.websockets:
ws.event("siteChanged", self, {"event": ["file_done", inner_path]})
-
def fileFailed(self, inner_path):
super(SitePlugin, self).fileFailed(inner_path)
@@ -294,7 +299,11 @@ class SiteManagerPlugin(object):
return
for site in self.sites.itervalues():
# Update merged sites
- merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
+ try:
+ merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
+ except Exception, err:
+ self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
+ continue
if merged_type:
merged_db[site.address] = merged_type
@@ -303,7 +312,10 @@ class SiteManagerPlugin(object):
if not permission.startswith("Merger:"):
continue
if merged_type:
- self.log.error("Removing permission %s from %s: Merger and merged at the same time." % (permission, site.address))
+ self.log.error(
+ "Removing permission %s from %s: Merger and merged at the same time." %
+ (permission, site.address)
+ )
site.settings["permissions"].remove(permission)
continue
merger_type = permission.replace("Merger:", "")
diff --git a/plugins/MergerSite/languages/fr.json b/plugins/MergerSite/languages/fr.json
new file mode 100644
index 00000000..9d59fde9
--- /dev/null
+++ b/plugins/MergerSite/languages/fr.json
@@ -0,0 +1,5 @@
+{
+ "Add %s new site?": "Ajouter le site %s ?",
+ "Added %s new site": "Site %s ajouté",
+ "Site deleted: %s": "Site %s supprimé"
+}
diff --git a/plugins/MergerSite/languages/tr.json b/plugins/MergerSite/languages/tr.json
new file mode 100644
index 00000000..5afb3942
--- /dev/null
+++ b/plugins/MergerSite/languages/tr.json
@@ -0,0 +1,5 @@
+{
+ "Add %s new site?": "%s sitesi eklensin mi?",
+ "Added %s new site": "%s sitesi eklendi",
+ "Site deleted: %s": "%s sitesi silindi"
+}
diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py
index 84a824fb..a083f621 100644
--- a/plugins/OptionalManager/ContentDbPlugin.py
+++ b/plugins/OptionalManager/ContentDbPlugin.py
@@ -157,7 +157,10 @@ class ContentDbPlugin(object):
def setContentFilesOptional(self, site, content_inner_path, content, cur=None):
if not cur:
cur = self
- cur.execute("BEGIN")
+ try:
+ cur.execute("BEGIN")
+ except Exception, err:
+ self.log.warning("Transaction begin error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
num = 0
site_id = self.site_ids[site.address]
@@ -190,8 +193,10 @@ class ContentDbPlugin(object):
num += 1
if cur == self:
- cur.execute("END")
-
+ try:
+ cur.execute("END")
+ except Exception, err:
+ self.log.warning("Transaction end error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
return num
def setContent(self, site, inner_path, content, size=0):
diff --git a/plugins/OptionalManager/languages/fr.json b/plugins/OptionalManager/languages/fr.json
new file mode 100644
index 00000000..47a563dc
--- /dev/null
+++ b/plugins/OptionalManager/languages/fr.json
@@ -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 %s.
Directory: %s": "Vous avez commencé à aider à distribuer %s.
Dossier : %s",
+ "Help distribute all new optional files on site %s": "Aider à distribuer tous les fichiers optionnels du site %s",
+ "Yes, I want to help!": "Oui, je veux aider !"
+}
diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index c9e71f13..08ed66df 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -191,7 +191,7 @@ class UiWebsocketPlugin(object):
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":
+ elif extension == "Total":
size = size_total
else:
size = size_filetypes.get(extension, 0)
@@ -411,12 +411,11 @@ class UiWebsocketPlugin(object):
# Choose content you want to sign
contents = ["content.json"]
contents += site.content_manager.contents.get("content.json", {}).get("includes", {}).keys()
- if len(contents) > 1:
- body.append(_(u"
@@ -561,7 +560,7 @@ class UiWebsocketPlugin(object):
globe_data += (lat, lon, ping)
# Append myself
loc = geodb.get(config.ip_external)
- if loc:
+ if loc and loc.get("location"):
lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
globe_data += (lat, lon, -0.135)
diff --git a/plugins/Sidebar/languages/es.json b/plugins/Sidebar/languages/es.json
new file mode 100644
index 00000000..b9e98c46
--- /dev/null
+++ b/plugins/Sidebar/languages/es.json
@@ -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: {}!
Please download manually and unpack to data dir:
{}": "¡Error de la base de datos GeoLite2: {}!
Por favor, descárgalo manualmente y descomprime al directorio de datos:
{}",
+ "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"
+}
diff --git a/plugins/Sidebar/languages/pl.json b/plugins/Sidebar/languages/pl.json
new file mode 100644
index 00000000..93268507
--- /dev/null
+++ b/plugins/Sidebar/languages/pl.json
@@ -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: {}!
Please download manually and unpack to data dir:
{}": "Błąd ściągania bazy danych GeoLite2 City: {}!
Proszę ściągnąć ją recznie i wypakować do katalogu danych:
{}",
+ "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"
+}
diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee
index c26c8008..44af3e3f 100644
--- a/plugins/Sidebar/media/Sidebar.coffee
+++ b/plugins/Sidebar/media/Sidebar.coffee
@@ -37,6 +37,8 @@ class Sidebar extends Class
# Detect dragging
@fixbutton.on "mousedown touchstart", (e) =>
+ if e.button > 0 # Right or middle click
+ return
e.preventDefault()
# Disable previous listeners
@@ -330,7 +332,7 @@ class Sidebar extends Class
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)], (res) =>
+ 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
diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js
index 058a46c9..c995a02e 100644
--- a/plugins/Sidebar/media/all.js
+++ b/plugins/Sidebar/media/all.js
@@ -237,6 +237,9 @@ window.initScrollable = function () {
*/
this.fixbutton.on("mousedown touchstart", (function(_this) {
return function(e) {
+ if (e.button > 0) {
+ return;
+ }
e.preventDefault();
_this.fixbutton.off("click touchstop touchcancel");
_this.fixbutton.off("mousemove touchmove");
@@ -569,7 +572,7 @@ window.initScrollable = function () {
data["title"] = $("#settings-title").val();
data["description"] = $("#settings-description").val();
json_raw = unescape(encodeURIComponent(JSON.stringify(data, void 0, '\t')));
- return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw)], function(res) {
+ return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw), true], function(res) {
if (res !== "ok") {
return wrapper.notifications.add("file-write", "error", "File write error: " + res);
} else {
diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py
index 75a0d4c5..426d17fa 100644
--- a/plugins/Stats/StatsPlugin.py
+++ b/plugins/Stats/StatsPlugin.py
@@ -130,7 +130,7 @@ class UiRequestPlugin(object):
# Db
yield "
Db:
"
for db in sys.modules["Db.Db"].opened_dbs:
- yield "- %.3fs: %s
" % (time.time() - db.last_query_time, db.db_path)
+ yield "- %.3fs: %s
" % (time.time() - db.last_query_time, db.db_path.encode("utf8"))
# Sites
yield "
Sites:"
@@ -220,7 +220,7 @@ class UiRequestPlugin(object):
objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
yield "
Greenlets (%s):
" % len(objs)
for obj in objs:
- yield " - %.1fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
+ yield " - %.1fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj).encode("utf8")))
from Worker import Worker
objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)]
@@ -401,7 +401,10 @@ class UiRequestPlugin(object):
except Exception, err:
output("
! Error: %s" % err)
taken = time.time() - s
- multipler = standard / taken
+ if taken > 0:
+ multipler = standard / taken
+ else:
+ multipler = 99
if multipler < 0.3:
speed = "Sloooow"
elif multipler < 0.5:
diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py
index a9254447..a3da23ff 100644
--- a/plugins/Trayicon/TrayiconPlugin.py
+++ b/plugins/Trayicon/TrayiconPlugin.py
@@ -1,4 +1,3 @@
-import time
import os
import sys
import atexit
@@ -12,6 +11,7 @@ allow_reload = False # No source reload supported in this plugin
if "_" not in locals():
_ = Translate("plugins/Trayicon/languages/")
+
@PluginManager.registerTo("Actions")
class ActionsPlugin(object):
@@ -54,23 +54,26 @@ class ActionsPlugin(object):
(_["ZeroNet Github"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet")),
(_["Report bug/request feature"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet/issues")),
"--",
- (_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage) )),
+ (_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))),
"--",
(_["Quit"], self.quit),
)
-
- icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage) )
+ icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))
+ self.quit_servers_event = gevent.threadpool.ThreadResult(
+ lambda res: gevent.spawn_later(0.1, self.quitServers)
+ ) # Fix gevent thread switch error
gevent.threadpool.start_new_thread(icon._run, ()) # Start in real thread (not gevent compatible)
super(ActionsPlugin, self).main()
icon._die = True
def quit(self):
self.icon.die()
- time.sleep(0.1)
- sys.exit()
- # self.main.ui_server.stop()
- # self.main.file_server.stop()
+ self.quit_servers_event.set(True)
+
+ def quitServers(self):
+ self.main.ui_server.stop()
+ self.main.file_server.stop()
def opensite(self, url):
import webbrowser
@@ -115,19 +118,33 @@ class ActionsPlugin(object):
def formatAutorun(self):
args = sys.argv[:]
- args.insert(0, sys.executable)
+
+ if not getattr(sys, 'frozen', False): # Not frozen
+ args.insert(0, sys.executable)
+ cwd = os.getcwd().decode(sys.getfilesystemencoding())
+ else:
+ cwd = os.path.dirname(sys.executable).decode(sys.getfilesystemencoding())
+
if sys.platform == 'win32':
- args = ['"%s"' % arg for arg in args]
+ args = ['"%s"' % arg for arg in args if arg]
cmd = " ".join(args)
# Dont open browser on autorun
cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip()
+ cmd += ' --open_browser ""'
+ cmd = cmd.decode(sys.getfilesystemencoding())
- return "@echo off\ncd /D %s\n%s" % (os.getcwd(), cmd)
+ return u"""
+ @echo off
+ chcp 65001
+ set PYTHONIOENCODING=utf-8
+ cd /D \"%s\"
+ %s
+ """ % (cwd, cmd)
def isAutorunEnabled(self):
path = self.getAutorunPath()
- return os.path.isfile(path) and open(path).read() == self.formatAutorun()
+ return os.path.isfile(path) and open(path).read().decode("utf8") == self.formatAutorun()
def titleAutorun(self):
translate = _["Start ZeroNet when Windows starts"]
@@ -140,4 +157,4 @@ class ActionsPlugin(object):
if self.isAutorunEnabled():
os.unlink(self.getAutorunPath())
else:
- open(self.getAutorunPath(), "w").write(self.formatAutorun())
+ open(self.getAutorunPath(), "w").write(self.formatAutorun().encode("utf8"))
diff --git a/plugins/Trayicon/languages/fr.json b/plugins/Trayicon/languages/fr.json
new file mode 100644
index 00000000..ec335318
--- /dev/null
+++ b/plugins/Trayicon/languages/fr.json
@@ -0,0 +1,14 @@
+{
+ "ZeroNet Twitter": "ZeroNet Twitter",
+ "ZeroNet Reddit": "ZeroNet Reddit",
+ "ZeroNet Github": "ZeroNet Github",
+ "Report bug/request feature": "Rapport d'erreur/Demanger une fonctionnalité",
+ "!Open ZeroNet": "!Ouvrir ZeroNet",
+ "Quit": "Quitter",
+ "(active)": "(actif)",
+ "(passive)": "(passif)",
+ "Connections: %s": "Connexions: %s",
+ "Received: %.2f MB | Sent: %.2f MB": "Reçu: %.2f MB | Envoyé: %.2f MB",
+ "Show console window": "Afficher la console",
+ "Start ZeroNet when Windows starts": "Lancer ZeroNet au démarrage de Windows"
+}
diff --git a/plugins/Trayicon/languages/tr.json b/plugins/Trayicon/languages/tr.json
new file mode 100644
index 00000000..077b8ddd
--- /dev/null
+++ b/plugins/Trayicon/languages/tr.json
@@ -0,0 +1,14 @@
+{
+ "ZeroNet Twitter": "ZeroNet Twitter",
+ "ZeroNet Reddit": "ZeroNet Reddit",
+ "ZeroNet Github": "ZeroNet Github",
+ "Report bug/request feature": "Hata bildir/geliştirme taleb et",
+ "!Open ZeroNet": "!ZeroNet'i Aç",
+ "Quit": "Kapat",
+ "(active)": "(aktif)",
+ "(passive)": "(pasif)",
+ "Connections: %s": "Bağlantı sayısı: %s",
+ "Received: %.2f MB | Sent: %.2f MB": "Gelen: %.2f MB | Gönderilen: %.2f MB",
+ "Show console window": "Konsolu aç",
+ "Start ZeroNet when Windows starts": "ZeroNet'i açılışta otomatik başlat"
+}
diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py
index 44cf58c9..94270363 100644
--- a/plugins/disabled-Bootstrapper/BootstrapperDb.py
+++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py
@@ -130,7 +130,7 @@ class BootstrapperDb(Db):
where = "hash_id = :hashid"
if onions:
- onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions]
+ onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str]
where += " AND (onion NOT IN (%s) OR onion IS NULL)" % ",".join(onions_escaped)
elif ip4:
where += " AND (NOT (ip4 = :ip4 AND port = :port) OR ip4 IS NULL)"
diff --git a/src/Config.py b/src/Config.py
index 1e7e5a5b..449cdcf0 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -2,6 +2,7 @@ import argparse
import sys
import os
import locale
+import re
import ConfigParser
@@ -9,7 +10,7 @@ class Config(object):
def __init__(self, argv):
self.version = "0.5.1"
- self.rev = 1766
+ self.rev = 1848
self.argv = argv
self.action = None
self.config_file = "zeronet.conf"
@@ -37,7 +38,7 @@ class Config(object):
"udp://tracker.coppersurfer.tk:6969",
"udp://tracker.leechers-paradise.org:6969",
"udp://9.rarbg.com:2710",
- "http://tracker.tordb.ml:6881/announce",
+ "http://tracker.opentrackr.org:1337/announce",
"http://explodie.org:6969/announce",
"http://tracker1.wasabii.com.tw:6969/announce"
]
@@ -55,6 +56,35 @@ class Config(object):
use_openssl = True
+ if repr(1483108852.565) != "1483108852.565":
+ fix_float_decimals = True
+ else:
+ fix_float_decimals = False
+
+ this_file = os.path.abspath(__file__).replace("\\", "/")
+
+ if this_file.endswith("/Contents/Resources/core/src/Config.py"):
+ # Running as ZeroNet.app
+ if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")):
+ # Runnig from non-writeable directory, put data to Application Support
+ start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet").decode(sys.getfilesystemencoding())
+ else:
+ # Running from writeable directory put data next to .app
+ start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file).decode(sys.getfilesystemencoding())
+ config_file = start_dir + "/zeronet.conf"
+ data_dir = start_dir + "/data"
+ log_dir = start_dir + "/log"
+ elif this_file.endswith("/core/src/Config.py"):
+ # Running as exe or source is at Application Support directory, put var files to outside of core dir
+ start_dir = this_file.replace("/core/src/Config.py", "").decode(sys.getfilesystemencoding())
+ config_file = start_dir + "/zeronet.conf"
+ data_dir = start_dir + "/data"
+ log_dir = start_dir + "/log"
+ else:
+ config_file = "zeronet.conf"
+ data_dir = "data"
+ log_dir = "log"
+
# Main
action = self.subparsers.add_parser("main", help='Start UiServer and FileServer (default)')
@@ -76,6 +106,7 @@ class Config(object):
action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?')
action.add_argument('--inner_path', help='File you want to sign (default: content.json)',
default="content.json", metavar="inner_path")
+ action.add_argument('--remove_missing_optional', help='Remove optional files that is not present in the directory', action='store_true')
action.add_argument('--publish', help='Publish site after the signing', action='store_true')
# SitePublish
@@ -134,9 +165,9 @@ class Config(object):
self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true')
- self.parser.add_argument('--config_file', help='Path of config file', default="zeronet.conf", metavar="path")
- self.parser.add_argument('--data_dir', help='Path of data directory', default="data", metavar="path")
- self.parser.add_argument('--log_dir', help='Path of logging directory', default="log", metavar="path")
+ self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path")
+ self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path")
+ self.parser.add_argument('--log_dir', help='Path of logging directory', default=log_dir, metavar="path")
self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language')
self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
@@ -149,7 +180,7 @@ class Config(object):
self.parser.add_argument('--updatesite', help='Source code update site', default='1UPDatEDxnvHDo7TXvq6AEBARfNkyfxsp',
metavar='address')
self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='size')
- self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=10, type=int, metavar='connected_limit')
+ self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=6, type=int, metavar='connected_limit')
self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip')
self.parser.add_argument('--fileserver_port', help='FileServer bind port', default=15441, type=int, metavar='port')
@@ -173,6 +204,8 @@ class Config(object):
type='bool', choices=[True, False], default=False)
self.parser.add_argument("--msgpack_purepython", help='Use less memory, but a bit more CPU power',
type='bool', choices=[True, False], default=True)
+ self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification',
+ type='bool', choices=[True, False], default=fix_float_decimals)
self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript,
metavar='executable_path')
@@ -180,6 +213,7 @@ class Config(object):
self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
+ self.parser.add_argument('--tor_password', help='Tor controller password', metavar='password')
self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py
index 5fe06a97..7f171fd6 100644
--- a/src/Connection/Connection.py
+++ b/src/Connection/Connection.py
@@ -78,6 +78,11 @@ class Connection(object):
def badAction(self, weight=1):
self.bad_actions += weight
+ if self.bad_actions > 40:
+ self.close()
+ elif self.bad_actions > 20:
+ time.sleep(5)
+
def goodAction(self):
self.bad_actions = 0
diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py
index 9d5d859c..c55b125f 100644
--- a/src/Connection/ConnectionServer.py
+++ b/src/Connection/ConnectionServer.py
@@ -55,7 +55,7 @@ class ConnectionServer:
if port: # Listen server on a port
self.pool = Pool(1000) # do not accept more than 1000 connections
self.stream_server = StreamServer(
- (ip.replace("*", ""), port), self.handleIncomingConnection, spawn=self.pool, backlog=500
+ (ip.replace("*", "0.0.0.0"), port), self.handleIncomingConnection, spawn=self.pool, backlog=500
)
if request_handler:
self.handleRequest = request_handler
diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 13142c13..6a23439c 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -212,7 +212,7 @@ class ContentManager(object):
# Update the content
self.contents[content_inner_path] = new_content
except Exception, err:
- self.log.warning("Content.json parse error: %s" % Debug.formatException(err))
+ self.log.warning("%s parse error: %s" % (content_inner_path, Debug.formatException(err)))
return [], [] # Content.json parse error
# Add changed files to bad files
@@ -315,6 +315,7 @@ class ContentManager(object):
if back:
back["content_inner_path"] = content_inner_path
back["optional"] = False
+ back["relative_path"] = "/".join(inner_path_parts)
return back
# Check in optional files
@@ -323,6 +324,7 @@ class ContentManager(object):
if back:
back["content_inner_path"] = content_inner_path
back["optional"] = True
+ back["relative_path"] = "/".join(inner_path_parts)
return back
# Return the rules if user dir
@@ -424,7 +426,7 @@ class ContentManager(object):
# Get diffs for changed files
def getDiffs(self, inner_path, limit=30 * 1024, update_files=True):
if inner_path not in self.contents:
- return None
+ return {}
diffs = {}
content_inner_path_dir = helper.getDirname(inner_path)
for file_relative_path in self.contents[inner_path].get("files", {}):
@@ -491,7 +493,7 @@ class ContentManager(object):
# Create and sign a content.json
# Return: The new content if filewrite = False
- def sign(self, inner_path="content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None):
+ def sign(self, inner_path="content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None, remove_missing_optional=False):
if inner_path in self.contents:
content = self.contents[inner_path]
if self.contents[inner_path].get("cert_sign", False) is None and self.site.storage.isFile(inner_path):
@@ -523,6 +525,11 @@ class ContentManager(object):
helper.getDirname(inner_path), content.get("ignore"), content.get("optional")
)
+ if not remove_missing_optional:
+ for file_inner_path, file_details in content.get("files_optional", {}).iteritems():
+ if file_inner_path not in files_optional_node:
+ files_optional_node[file_inner_path] = file_details
+
# Find changed files
files_merged = files_node.copy()
files_merged.update(files_optional_node)
@@ -547,7 +554,7 @@ class ContentManager(object):
elif "files_optional" in new_content:
del new_content["files_optional"]
- new_content["modified"] = time.time() # Add timestamp
+ new_content["modified"] = int(time.time()) # Add timestamp
if inner_path == "content.json":
new_content["zeronet_version"] = config.version
new_content["signs_required"] = content.get("signs_required", 1)
@@ -768,8 +775,18 @@ class ContentManager(object):
del(new_content["sign"]) # The file signed without the sign
if "signs" in new_content:
del(new_content["signs"]) # The file signed without the signs
+
sign_content = json.dumps(new_content, sort_keys=True) # Dump the json to string to remove whitepsace
+ # Fix float representation error on Android
+ modified = new_content["modified"]
+ if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"):
+ modified_fixed = "{:.6f}".format(modified).strip("0.")
+ sign_content = sign_content.replace(
+ '"modified": %s' % repr(modified),
+ '"modified": %s' % modified_fixed
+ )
+
if not self.verifyContent(inner_path, new_content):
return False # Content not valid (files too large, invalid files)
diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py
index 61d96acc..b30c7e49 100644
--- a/src/Crypt/CryptConnection.py
+++ b/src/Crypt/CryptConnection.py
@@ -70,13 +70,14 @@ class CryptConnectionManager:
return True # Files already exits
import subprocess
+ cmd = "%s req -x509 -newkey rsa:2048 -sha256 -batch -keyout %s -out %s -nodes -config %s" % helper.shellquote(
+ self.openssl_bin,
+ config.data_dir+"/key-rsa.pem",
+ config.data_dir+"/cert-rsa.pem",
+ self.openssl_env["OPENSSL_CONF"]
+ )
proc = subprocess.Popen(
- "%s req -x509 -newkey rsa:2048 -sha256 -batch -keyout %s -out %s -nodes -config %s" % helper.shellquote(
- self.openssl_bin,
- config.data_dir+"/key-rsa.pem",
- config.data_dir+"/cert-rsa.pem",
- self.openssl_env["OPENSSL_CONF"]
- ),
+ cmd.encode(sys.getfilesystemencoding()),
shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env
)
back = proc.stdout.read().strip()
diff --git a/src/Db/Db.py b/src/Db/Db.py
index 8609a7f4..be702e22 100644
--- a/src/Db/Db.py
+++ b/src/Db/Db.py
@@ -55,7 +55,7 @@ class Db(object):
self.log.debug("Created Db path: %s" % self.db_dir)
if not os.path.isfile(self.db_path):
self.log.debug("Db file not exist yet: %s" % self.db_path)
- self.conn = sqlite3.connect(self.db_path)
+ self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self.conn.isolation_level = None
self.cur = self.getCursor()
diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py
index 02dfffcb..ce30ab3c 100644
--- a/src/Debug/DebugReloader.py
+++ b/src/Debug/DebugReloader.py
@@ -40,9 +40,11 @@ class DebugReloader:
if (
not evt.path or "%s/" % config.data_dir in evt.path or
(not evt.path.endswith("py") and not evt.path.endswith("json")) or
+ "Test" in evt.path or
time.time() - self.last_chaged < 1
):
return False # Ignore *.pyc changes and no reload within 1 sec
time.sleep(0.1) # Wait for lock release
+ logging.debug("Changed: %s, reloading source." % evt.path)
self.callback()
self.last_chaged = time.time()
diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py
index c0c28e64..cdbf2302 100644
--- a/src/File/FileRequest.py
+++ b/src/File/FileRequest.py
@@ -173,7 +173,9 @@ class FileRequest(object):
file.seek(params["location"])
file.read_bytes = FILE_BUFF
file_size = os.fstat(file.fileno()).st_size
- assert params["location"] <= file_size, "Bad file location"
+ if params["location"] > file_size:
+ self.connection.badAction(5)
+ raise Exception("Bad file location")
back = {
"body": file,
@@ -212,7 +214,9 @@ class FileRequest(object):
file.seek(params["location"])
file_size = os.fstat(file.fileno()).st_size
stream_bytes = min(FILE_BUFF, file_size - params["location"])
- assert stream_bytes >= 0, "Stream bytes out of range"
+ if stream_bytes < 0:
+ self.connection.badAction(5)
+ raise Exception("Bad file location")
back = {
"size": file_size,
diff --git a/src/Site/Site.py b/src/Site/Site.py
index d2527a79..e5a9cfb9 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -398,7 +398,7 @@ class Site(object):
gevent.joinall(content_threads)
# Publish worker
- def publisher(self, inner_path, peers, published, limit, event_done=None, diffs={}):
+ def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None):
file_size = self.storage.getSize(inner_path)
content_json_modified = self.content_manager.contents[inner_path]["modified"]
body = self.storage.read(inner_path)
@@ -457,6 +457,8 @@ class Site(object):
if result and "ok" in result:
published.append(peer)
+ if cb_progress and len(published) <= limit:
+ cb_progress(len(published), limit)
self.log.info("[OK] %s: %s %s/%s" % (peer.key, result["ok"], len(published), limit))
else:
if result == {"exception": "Timeout"}:
@@ -466,7 +468,7 @@ class Site(object):
# Update content.json on peers
@util.Noparallel()
- def publish(self, limit="default", inner_path="content.json", diffs={}):
+ def publish(self, limit="default", inner_path="content.json", diffs={}, cb_progress=None):
published = [] # Successfully published (Peer)
publishers = [] # Publisher threads
@@ -498,7 +500,7 @@ class Site(object):
event_done = gevent.event.AsyncResult()
for i in range(min(len(peers), limit, threads)):
- publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit, event_done, diffs)
+ publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit, diffs, event_done, cb_progress)
publishers.append(publisher)
event_done.get() # Wait for done
@@ -522,7 +524,7 @@ class Site(object):
return len(published)
# Copy this site
- def clone(self, address, privatekey=None, address_index=None, overwrite=False):
+ def clone(self, address, privatekey=None, address_index=None, root_inner_path="", overwrite=False):
import shutil
new_site = SiteManager.site_manager.need(address, all_file=False)
default_dirs = [] # Dont copy these directories (has -default version)
@@ -530,16 +532,20 @@ class Site(object):
if "-default" in dir_name:
default_dirs.append(dir_name.replace("-default", ""))
- self.log.debug("Cloning to %s, ignore dirs: %s" % (address, default_dirs))
+ self.log.debug("Cloning to %s, ignore dirs: %s, root: %s" % (address, default_dirs, root_inner_path))
# Copy root content.json
if not new_site.storage.isFile("content.json") and not overwrite:
# Content.json not exist yet, create a new one from source site
- content_json = self.storage.loadJson("content.json")
+ if self.storage.isFile(root_inner_path + "/content.json-default"):
+ content_json = self.storage.loadJson(root_inner_path + "/content.json-default")
+ else:
+ content_json = self.storage.loadJson("content.json")
if "domain" in content_json:
del content_json["domain"]
content_json["title"] = "my" + content_json["title"]
content_json["cloned_from"] = self.address
+ content_json["clone_root"] = root_inner_path
content_json["files"] = {}
if address_index:
content_json["address_index"] = address_index # Site owner's BIP32 index
@@ -553,17 +559,29 @@ class Site(object):
for file_relative_path in sorted(content["files"].keys()):
file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to content.json
file_inner_path = file_inner_path.strip("/") # Strip leading /
+ if not file_inner_path.startswith(root_inner_path):
+ self.log.debug("[SKIP] %s (not in clone root)" % file_inner_path)
+ continue
if file_inner_path.split("/")[0] in default_dirs: # Dont copy directories that has -default postfixed alternative
self.log.debug("[SKIP] %s (has default alternative)" % file_inner_path)
continue
file_path = self.storage.getPath(file_inner_path)
# Copy the file normally to keep the -default postfixed dir and file to allow cloning later
- file_path_dest = new_site.storage.getPath(file_inner_path)
+ if root_inner_path:
+ file_inner_path_dest = re.sub("^%s/" % re.escape(root_inner_path), "", file_inner_path)
+ file_path_dest = new_site.storage.getPath(file_inner_path_dest)
+ else:
+ file_inner_path_dest = file_inner_path
+ file_path_dest = new_site.storage.getPath(file_inner_path)
+
self.log.debug("[COPY] %s to %s..." % (file_inner_path, file_path_dest))
dest_dir = os.path.dirname(file_path_dest)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
+ if file_inner_path_dest == "content.json-default": # Don't copy root content.json-default
+ continue
+
shutil.copy(file_path, file_path_dest)
# If -default in path, create a -default less copy of the file
diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index 6f35d13f..8a2a6a5b 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -36,7 +36,13 @@ class SiteManager(object):
for address, settings in json.load(open("%s/sites.json" % config.data_dir)).iteritems():
if address not in self.sites and os.path.isfile("%s/%s/content.json" % (config.data_dir, address)):
s = time.time()
- self.sites[address] = Site(address, settings=settings)
+ try:
+ site = Site(address, settings=settings)
+ site.content_manager.contents.get("content.json")
+ except Exception, err:
+ self.log.debug("Error loading site %s: %s" % (address, err))
+ continue
+ self.sites[address] = site
self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s))
added += 1
address_found.append(address)
diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index f9d7c187..1c7b6151 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -19,8 +19,8 @@ from Plugin import PluginManager
class SiteStorage(object):
def __init__(self, site, allow_create=True):
self.site = site
- self.directory = "%s/%s" % (config.data_dir, self.site.address) # Site data diretory
- self.allowed_dir = os.path.abspath(self.directory.decode(sys.getfilesystemencoding())) # Only serve file within this dir
+ self.directory = u"%s/%s" % (config.data_dir, self.site.address) # Site data diretory
+ self.allowed_dir = os.path.abspath(self.directory) # Only serve file within this dir
self.log = site.log
self.db = None # Db class
self.db_checked = False # Checked db tables since startup
@@ -166,8 +166,11 @@ class SiteStorage(object):
with open(file_path, "wb") as file:
shutil.copyfileobj(content, file) # Write buff to disk
else: # Simple string
- with open(file_path, "wb") as file:
- file.write(content)
+ if inner_path == "content.json" and os.path.isfile(file_path):
+ helper.atomicWrite(file_path, content)
+ else:
+ with open(file_path, "wb") as file:
+ file.write(content)
del content
self.onUpdated(inner_path)
@@ -275,11 +278,10 @@ class SiteStorage(object):
if not inner_path:
return self.directory
- file_path = u"%s/%s" % (self.directory, inner_path)
+ if ".." in inner_path:
+ raise Exception(u"File not allowed: %s" % inner_path)
- if ".." in file_path:
- raise Exception(u"File not allowed: %s" % file_path)
- return file_path
+ return u"%s/%s" % (self.directory, inner_path)
# Get site dir relative path
def getInnerPath(self, path):
@@ -415,8 +417,8 @@ class SiteStorage(object):
os.unlink(path)
break
except Exception, err:
- self.log.error("Error removing %s: %s, try #%s" % (path, err, retry))
- time.sleep(float(retry)/10)
+ self.log.error("Error removing %s: %s, try #%s" % (path, err, retry))
+ time.sleep(float(retry) / 10)
self.onUpdated(inner_path, False)
self.log.debug("Deleting empty dirs...")
diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py
index df62aec4..78065c00 100644
--- a/src/Test/TestContent.py
+++ b/src/Test/TestContent.py
@@ -108,10 +108,10 @@ class TestContent:
assert len(site.content_manager.hashfield) == 0
site.content_manager.contents["content.json"]["optional"] = "((data/img/zero.*))"
- content_optional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False)
+ content_optional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True)
del site.content_manager.contents["content.json"]["optional"]
- content_nooptional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False)
+ content_nooptional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True)
assert len(content_nooptional.get("files_optional", {})) == 0 # No optional files if no pattern
assert len(content_optional["files_optional"]) > 0
diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py
index abc4c767..736bf4ea 100644
--- a/src/Test/TestNoparallel.py
+++ b/src/Test/TestNoparallel.py
@@ -1,10 +1,7 @@
import time
-import gevent
-from gevent import monkey
-monkey.patch_all()
-
import util
+import gevent
class ExampleClass(object):
def __init__(self):
diff --git a/src/Test/TestRateLimit.py b/src/Test/TestRateLimit.py
index a823d88b..b0a91ba0 100644
--- a/src/Test/TestRateLimit.py
+++ b/src/Test/TestRateLimit.py
@@ -1,8 +1,6 @@
import time
import gevent
-from gevent import monkey
-monkey.patch_all()
from util import RateLimit
diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py
index 72a34a5a..5cc6825c 100644
--- a/src/Test/TestWeb.py
+++ b/src/Test/TestWeb.py
@@ -18,14 +18,14 @@ class WaitForPageLoad(object):
self.old_page = self.browser.find_element_by_tag_name('html')
def __exit__(self, *args):
- WebDriverWait(self.browser, 20).until(staleness_of(self.old_page))
+ WebDriverWait(self.browser, 5).until(staleness_of(self.old_page))
@pytest.mark.usefixtures("resetSettings")
@pytest.mark.webtest
class TestWeb:
def testFileSecurity(self, site_url):
- assert "Forbidden" in urllib.urlopen("%s/media/./sites.json" % site_url).read()
+ assert "Not Found" in urllib.urlopen("%s/media/./sites.json" % site_url).read()
assert "Forbidden" in urllib.urlopen("%s/media/../config.py" % site_url).read()
assert "Forbidden" in urllib.urlopen("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url).read()
assert "Forbidden" in urllib.urlopen("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url).read()
@@ -34,10 +34,6 @@ class TestWeb:
assert "Forbidden" in urllib.urlopen("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url).read()
assert "Forbidden" in urllib.urlopen("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../../zeronet.py" % site_url).read()
- def testHomepage(self, browser, site_url):
- browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr" % site_url)
- assert browser.title == "ZeroHello - ZeroNet"
-
def testLinkSecurity(self, browser, site_url):
browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url)
assert browser.title == "ZeroHello - ZeroNet"
diff --git a/src/Test/conftest.py b/src/Test/conftest.py
index 905aa2b0..1388bd11 100644
--- a/src/Test/conftest.py
+++ b/src/Test/conftest.py
@@ -49,7 +49,7 @@ if os.path.isfile("%s-temp/content.db" % config.data_dir):
import gevent
from gevent import monkey
-monkey.patch_all(thread=False)
+monkey.patch_all(thread=False, subprocess=False)
from Site import Site
from Site import SiteManager
@@ -164,7 +164,9 @@ def user():
def browser():
try:
from selenium import webdriver
+ print "Starting phantomjs..."
browser = webdriver.PhantomJS(executable_path=PHANTOMJS_PATH, service_log_path=os.path.devnull)
+ print "Set window size..."
browser.set_window_size(1400, 1000)
except Exception, err:
raise pytest.skip("Test requires selenium + phantomjs: %s" % err)
diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py
index b01ad794..b91cd937 100644
--- a/src/Tor/TorManager.py
+++ b/src/Tor/TorManager.py
@@ -14,7 +14,10 @@ from Config import config
from Crypt import CryptRsa
from Site import SiteManager
from lib.PySocks import socks
-from gevent.coros import RLock
+try:
+ from gevent.coros import RLock
+except:
+ from gevent.lock import RLock
from util import helper
from Debug import Debug
@@ -75,7 +78,9 @@ class TorManager:
self.log.info("Starting Tor client %s..." % self.tor_exe)
tor_dir = os.path.dirname(self.tor_exe)
- self.tor_process = subprocess.Popen(r"%s -f torrc" % self.tor_exe, cwd=tor_dir, close_fds=True)
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ self.tor_process = subprocess.Popen(r"%s -f torrc" % self.tor_exe, cwd=tor_dir, close_fds=True, startupinfo=startupinfo)
for wait in range(1,10): # Wait for startup
time.sleep(wait * 0.5)
self.enabled = True
@@ -152,22 +157,26 @@ class TorManager:
try:
with self.lock:
conn.connect((self.ip, self.port))
- res_protocol = self.send("PROTOCOLINFO", conn)
-
- version = re.search('Tor="([0-9\.]+)', res_protocol).group(1)
- # Version 0.2.7.5 required because ADD_ONION support
- assert float(version.replace(".", "0", 2)) >= 207.5, "Tor version >=0.2.7.5 required, found: %s" % version
# Auth cookie file
+ res_protocol = self.send("PROTOCOLINFO", conn)
cookie_match = re.search('COOKIEFILE="(.*?)"', res_protocol)
if cookie_match:
- cookie_file = cookie_match.group(1)
+ cookie_file = cookie_match.group(1).decode("string-escape")
auth_hex = binascii.b2a_hex(open(cookie_file, "rb").read())
res_auth = self.send("AUTHENTICATE %s" % auth_hex, conn)
+ elif config.tor_password:
+ res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn)
else:
res_auth = self.send("AUTHENTICATE", conn)
assert "250 OK" in res_auth, "Authenticate error %s" % res_auth
+
+ # Version 0.2.7.5 required because ADD_ONION support
+ res_version = self.send("GETINFO version", conn)
+ version = re.search('version=([0-9\.]+)', res_version).group(1)
+ assert float(version.replace(".", "0", 2)) >= 207.5, "Tor version >=0.2.7.5 required, found: %s" % version
+
self.status = u"Connected (%s)" % res_auth
self.conn = conn
except Exception, err:
@@ -232,10 +241,12 @@ class TorManager:
if not conn:
conn = self.conn
self.log.debug("> %s" % cmd)
+ back = ""
for retry in range(2):
try:
- conn.send("%s\r\n" % cmd)
- back = conn.recv(1024 * 64).decode("utf8", "ignore")
+ conn.sendall("%s\r\n" % cmd)
+ while not back.endswith("250 OK\r\n"):
+ back += conn.recv(1024 * 64).decode("utf8", "ignore")
break
except Exception, err:
self.log.error("Tor send error: %s, reconnecting..." % err)
diff --git a/src/Translate/languages/es.json b/src/Translate/languages/es.json
new file mode 100644
index 00000000..659dc0e9
--- /dev/null
+++ b/src/Translate/languages/es.json
@@ -0,0 +1,51 @@
+{
+ "Congratulation, your port
{0} is opened.
You are full member of ZeroNet network!": "¡Felicidades! tu puerto
{0} está abierto.
¡Eres un miembro completo de la red Zeronet!",
+ "Tor mode active, every connection using Onion route.": "Modo Tor activado, cada conexión usa una ruta Onion.",
+ "Successfully started Tor onion hidden services.": "Tor ha iniciado satisfactoriamente la ocultación de los servicios onion.",
+ "Unable to start hidden services, please check your config.": "No se puedo iniciar los servicios ocultos, por favor comprueba tu configuración.",
+ "For faster connections open
{0} port on your router.": "Para conexiones más rápidas abre el puerto
{0} en tu router.",
+ "Your connection is restricted. Please, open
{0} port on your router": "Tu conexión está limitada. Por favor, abre el puerto
{0} en tu router",
+ "or configure Tor to become full member of ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.",
+
+ "Select account you want to use in this site:": "Selecciona la cuenta que quieres utilizar en este sitio:",
+ "currently selected": "actualmente seleccionada",
+ "Unique to site": "Única para el sitio",
+
+ "Content signing failed": "Firma del contenido fallida",
+ "Content publish queued for {0:.0f} seconds.": "Publicación de contenido en cola durante {0:.0f} segundos.",
+ "Content published to {0} peers.": "Contenido publicado para {0} pares.",
+ "No peers found, but your content is ready to access.": "No se ha encontrado pares, pero tu contenido está listo para ser accedido.",
+ "Your network connection is restricted. Please, open
{0} port": "Tu conexión de red está restringida. Por favor, abre el puerto
{0}",
+ "on your router to make your site accessible for everyone.": "en tu router para hacer tu sitio accesible a todo el mundo.",
+ "Content publish failed.": "Publicación de contenido fallida.",
+ "This file still in sync, if you write it now, then the previous content may be lost.": "Este archivo está aún sincronizado, si le escribes ahora el contenido previo podría perderse.",
+ "Write content anyway": "Escribir el contenido de todas formas",
+ "New certificate added:": "Nuevo certificado añadido:",
+ "You current certificate:": "Tu certificado actual:",
+ "Change it to {auth_type}/{auth_user_name}@{domain}": "Cambia esto a {auth_type}/{auth_user_name}@{domain}",
+ "Certificate changed to:
{auth_type}/{auth_user_name}@{domain}.": "Certificado cambiado a:
{auth_type}/{auth_user_name}@{domain}.",
+ "Site cloned": "Sitio clonado",
+
+ "You have successfully changed the web interface's language!": "¡Has cambiado con éxito el idioma de la interfaz web!",
+ "Due to the browser's caching, the full transformation could take some minute.": "Debido a la caché del navegador, la transformación completa podría llevar unos minutos.",
+
+ "Connection with
UiServer Websocket was lost. Reconnecting...": "Se perdió la conexión con
UiServer Websocket. Reconectando...",
+ "Connection with
UiServer Websocket recovered.": "Conexión con
UiServer Websocket recuperada.",
+ "UiServer Websocket error, please reload the page.": "Error de UiServer Websocket, por favor recarga la página.",
+ " Connecting...": " Conectando...",
+ "Site size:
": "Tamaño del sitio: ",
+ "MB is larger than default allowed ": "MB es más grande de lo permitido por defecto",
+ "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Abre tu sitio and establece el límite de tamaño a \" + site_info.next_size_limit + \"MBs",
+ " files needs to be downloaded": " Los archivos necesitan ser descargados",
+ " downloaded": " descargados",
+ " download failed": " descarga fallida",
+ "Peers found: ": "Pares encontrados: ",
+ "No peers found": "No se han encontrado pares",
+ "Running out of size limit (": "Superando el tamaño límite (",
+ "Set limit to \" + site_info.next_size_limit + \"MB": "Establece ellímite a \" + site_info.next_size_limit + \"MB ändern",
+ "Site size limit changed to {0}MB": "Límite de tamaño del sitio cambiado a {0}MBs",
+ " New version of this page has just released.
Reload to see the modified content.": " Se ha publicado una nueva versión de esta página .
Recarga para ver el contenido modificado.",
+ "This site requests permission:": "Este sitio solicita permiso:",
+ "Grant": "Conceder"
+
+}
diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json
new file mode 100644
index 00000000..e3087c73
--- /dev/null
+++ b/src/Translate/languages/pl.json
@@ -0,0 +1,51 @@
+{
+ "Congratulation, your port
{0} is opened.
You are full member of ZeroNet network!": "Gratulacje, twój port
{0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!",
+ "Tor mode active, every connection using Onion route.": "Tryb Tor aktywny, każde połączenie przy użyciu trasy Cebulowej.",
+ "Successfully started Tor onion hidden services.": "Pomyślnie zainicjowano ukryte usługi cebulowe Tor.",
+ "Unable to start hidden services, please check your config.": "Niezdolny do uruchomienia ukrytych usług, proszę sprawdź swoją konfigurację.",
+ "For faster connections open
{0} port on your router.": "Dla szybszego połączenia otwórz
{0} port w swoim routerze.",
+ "Your connection is restricted. Please, open
{0} port on your router": "Połączenie jest ograniczone. Proszę, otwórz port
{0} w swoim routerze",
+ "or configure Tor to become full member of ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.",
+
+ "Select account you want to use in this site:": "Wybierz konto którego chcesz użyć na tej stronie:",
+ "currently selected": "aktualnie wybrany",
+ "Unique to site": "Unikatowy dla strony",
+
+ "Content signing failed": "Podpisanie treści zawiodło",
+ "Content publish queued for {0:.0f} seconds.": "Publikacja treści wstrzymana na {0:.0f} sekund(y).",
+ "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników równorzednych.",
+ "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników równorzędnych, ale twoja treść jest dostępna.",
+ "Your network connection is restricted. Please, open
{0} port": "Twoje połączenie sieciowe jest ograniczone. Proszę, otwórz port
{0}",
+ "on your router to make your site accessible for everyone.": "w swoim routerze, by twoja strona mogłabyć dostępna dla wszystkich.",
+ "Content publish failed.": "Publikacja treści zawiodła.",
+ "This file still in sync, if you write it now, then the previous content may be lost.": "Ten plik wciąż się synchronizuje, jeśli zapiszesz go teraz, poprzednia treść może zostać utracona.",
+ "Write content anyway": "Zapisz treść mimo wszystko",
+ "New certificate added:": "Nowy certyfikat dodany:",
+ "You current certificate:": "Twój aktualny certyfikat: ",
+ "Change it to {auth_type}/{auth_user_name}@{domain}": "Zmień na {auth_type}/{auth_user_name}@{domain}-ra",
+ "Certificate changed to:
{auth_type}/{auth_user_name}@{domain}.": "Certyfikat zmieniony na
{auth_type}/{auth_user_name}@{domain}-ra.",
+ "Site cloned": "Strona sklonowana",
+
+ "You have successfully changed the web interface's language!": "Pomyślnie zmieniono język interfejsu stron!",
+ "Due to the browser's caching, the full transformation could take some minute.": "Ze względu na buforowanie przeglądarki, pełna zmiana może zająć parę minutę.",
+
+ "Connection with
UiServer Websocket was lost. Reconnecting...": "Połączenie z
UiServer Websocket zostało przerwane. Ponowne łączenie...",
+ "Connection with
UiServer Websocket recovered.": "Połączenie z
UiServer Websocket przywrócone.",
+ "UiServer Websocket error, please reload the page.": "Błąd UiServer Websocket, prosze odświeżyć stronę.",
+ " Connecting...": " Łączenie...",
+ "Site size:
": "Rozmiar strony: ",
+ "MB is larger than default allowed ": "MB jest większy niż domyślnie dozwolony ",
+ "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Otwórz stronę i ustaw limit na \" + site_info.next_size_limit + \"MBów",
+ " files needs to be downloaded": " pliki muszą zostać ściągnięte",
+ " downloaded": " ściągnięte",
+ " download failed": " ściąganie nie powiodło się",
+ "Peers found: ": "Odnaleziono użytkowników równorzednych: ",
+ "No peers found": "Nie odnaleziono użytkowników równorzędnych",
+ "Running out of size limit (": "Limit rozmiaru na wyczerpaniu (",
+ "Set limit to \" + site_info.next_size_limit + \"MB": "Ustaw limit na \" + site_info.next_size_limit + \"MBów",
+ "Site size limit changed to {0}MB": "Rozmiar limitu strony zmieniony na {0}MBów",
+ " New version of this page has just released.
Reload to see the modified content.": "Nowa wersja tej strony właśnie została wydana.
Odśwież by zobaczyć nową, zmodyfikowaną treść strony.",
+ "This site requests permission:": "Ta strona wymaga uprawnień:",
+ "Grant": "Przyznaj uprawnienia"
+
+}
diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 046e55c6..af404c31 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -49,7 +49,10 @@ class UiRequest(object):
path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access
if self.env["REQUEST_METHOD"] == "OPTIONS":
- content_type = self.getContentType(path)
+ if "/" not in path.strip("/"):
+ content_type = self.getContentType("index.html")
+ else:
+ content_type = self.getContentType(path)
self.sendHeader(content_type=content_type)
return ""
@@ -64,8 +67,6 @@ class UiRequest(object):
# uimedia within site dir (for chrome extension)
path = re.sub(".*?/uimedia/", "/uimedia/", path)
return self.actionUiMedia(path)
- elif path.startswith("/media"):
- return self.actionSiteMedia(path)
# Websocket
elif path == "/Websocket":
return self.actionWebsocket()
@@ -93,14 +94,21 @@ class UiRequest(object):
def isProxyRequest(self):
return self.env["PATH_INFO"].startswith("http://")
+ def isWebSocketRequest(self):
+ return self.env.get("HTTP_UPGRADE") == "websocket"
+
def isAjaxRequest(self):
return self.env.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
# Get mime by filename
def getContentType(self, file_name):
content_type = mimetypes.guess_type(file_name)[0]
+
+ if file_name.endswith(".css"): # Force correct css content type
+ content_type = "text/css"
+
if not content_type:
- if file_name.endswith("json"): # Correct json header
+ if file_name.endswith(".json"): # Correct json header
content_type = "application/json"
else:
content_type = "application/octet-stream"
@@ -140,6 +148,7 @@ class UiRequest(object):
headers.append(("Keep-Alive", "max=25, timeout=30"))
if content_type != "text/html":
headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access on non-html files
+ headers.append(("X-Frame-Options", "SAMEORIGIN"))
# headers.append(("Content-Security-Policy", "default-src 'self' data: 'unsafe-inline' ws://127.0.0.1:* http://127.0.0.1:* wss://tracker.webtorrent.io; sandbox allow-same-origin allow-top-navigation allow-scripts")) # Only local connections
if self.env["REQUEST_METHOD"] == "OPTIONS":
# Allow json access
@@ -191,6 +200,14 @@ class UiRequest(object):
if self.isAjaxRequest():
return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on wrapper
+ if self.isWebSocketRequest():
+ return self.error403("WebSocket request not allowed to load wrapper") # No websocket
+
+ if "text/html" not in self.env.get("HTTP_ACCEPT", ""):
+ return self.error403("Invalid Accept header to load wrapper")
+ if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""):
+ return self.error403("Prefetch not allowed to load wrapper")
+
site = SiteManager.site_manager.get(address)
if (
@@ -339,14 +356,8 @@ class UiRequest(object):
if path_parts: # Looks like a valid path
address = path_parts["address"]
file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"])
- allowed_dir = os.path.abspath("%s/%s" % (config.data_dir, address)) # Only files within data/sitehash allowed
- data_dir = os.path.abspath(config.data_dir) # No files from data/ allowed
- if (
- ".." in file_path or
- not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir) or
- allowed_dir == data_dir
- ): # File not in allowed path
- return self.error403()
+ if ".." in path_parts["inner_path"]: # File not in allowed path
+ return self.error403("Invalid file path")
else:
if config.debug and file_path.split("/")[-1].startswith("all."):
# If debugging merge *.css to all.css and *.js to all.js
@@ -359,7 +370,10 @@ class UiRequest(object):
elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect
return self.actionRedirect("./{0}/".format(path_parts["inner_path"].split("/")[-1]))
else: # File not exists, try to download
- site = SiteManager.site_manager.need(address, all_file=False)
+ if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading
+ return self.error404(path_parts["inner_path"])
+
+ site = SiteManager.site_manager.need(address)
if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites
return self.actionFile("src/Ui/media/img/favicon.ico")
diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 745ec512..5592596d 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -3,6 +3,7 @@ import time
import cgi
import socket
import sys
+import gevent
from gevent.pywsgi import WSGIServer
from gevent.pywsgi import WSGIHandler
@@ -121,7 +122,8 @@ class UiServer:
browser = webbrowser.get()
else:
browser = webbrowser.get(config.open_browser)
- browser.open("http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage), new=2)
+ url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage)
+ gevent.spawn_later(0.3, browser.open, url, new=2)
self.server = WSGIServer((self.ip.replace("*", ""), self.port), handler, handler_class=UiWSGIHandler, log=self.log)
self.server.sockets = {}
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index ff147a81..7408e9a3 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -32,6 +32,12 @@ class UiWebsocket(object):
self.channels = [] # Channels joined to
self.sending = False # Currently sending to client
self.send_queue = [] # Messages to send to client
+ self.admin_commands = (
+ "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone",
+ "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet",
+ "actionPermissionAdd", "actionPermissionRemove"
+ )
+ self.async_commands = ("fileGet", "fileList")
# Start listener loop
def start(self):
@@ -152,6 +158,11 @@ class UiWebsocket(object):
permissions.append("ADMIN")
return permissions
+ def asyncWrapper(self, func):
+ def wrapper(*args, **kwargs):
+ gevent.spawn(func, *args, **kwargs)
+ return wrapper
+
# Handle incoming messages
def handleRequest(self, data):
req = json.loads(data)
@@ -160,16 +171,10 @@ class UiWebsocket(object):
params = req.get("params")
self.permissions = self.getPermissions(req["id"])
- admin_commands = (
- "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone",
- "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet",
- "actionPermissionAdd", "actionPermissionRemove"
- )
-
if cmd == "response": # It's a response to a command
return self.actionResponse(req["to"], req["result"])
- elif cmd in admin_commands and "ADMIN" not in self.permissions: # Admin commands
- return self.response(req["id"], {"error:", "You don't have permission to run %s" % cmd})
+ elif cmd in self.admin_commands and "ADMIN" not in self.permissions: # Admin commands
+ return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd})
else: # Normal command
func_name = "action" + cmd[0].upper() + cmd[1:]
func = getattr(self, func_name, None)
@@ -177,6 +182,10 @@ class UiWebsocket(object):
self.response(req["id"], {"error": "Unknown command: %s" % cmd})
return
+ # Execute in parallel
+ if cmd in self.async_commands:
+ func = self.asyncWrapper(func)
+
# Support calling as named, unnamed parameters and raw first argument too
if type(params) is dict:
func(req["id"], **params)
@@ -278,7 +287,7 @@ class UiWebsocket(object):
self.response(to, ret)
# Sign content.json
- def actionSiteSign(self, to, privatekey=None, inner_path="content.json", response_ok=True, update_changed_files=False):
+ def actionSiteSign(self, to, privatekey=None, inner_path="content.json", response_ok=True, update_changed_files=False, remove_missing_optional=False):
self.log.debug("Signing: %s" % inner_path)
site = self.site
extend = {} # Extended info for signing
@@ -311,7 +320,7 @@ class UiWebsocket(object):
# Reload content.json, ignore errors to make it up-to-date
site.content_manager.loadContent(inner_path, add_bad_files=False, force=True)
# Sign using private key sent by user
- signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files)
+ signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional)
if not signed:
self.cmd("notification", ["error", _["Content signing failed"]])
self.response(to, {"error": "Site sign failed"})
@@ -346,6 +355,7 @@ class UiWebsocket(object):
thread.linked = True
if called_instantly: # Allowed to call instantly
# At the end callback with request id and thread
+ self.cmd("progress", ["publish", _["Content published to {0}/{1} peers."].format(0, 5), 0])
thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=notification))
else:
self.cmd(
@@ -357,15 +367,27 @@ class UiWebsocket(object):
thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=False))
def doSitePublish(self, site, inner_path):
+ def cbProgress(published, limit):
+ progress = int(float(published) / limit * 100)
+ self.cmd("progress", [
+ "publish",
+ _["Content published to {0}/{1} peers."].format(published, limit),
+ progress
+ ])
diffs = site.content_manager.getDiffs(inner_path)
- return site.publish(limit=5, inner_path=inner_path, diffs=diffs)
+ back = site.publish(limit=5, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress)
+ if back == 0: # Failed to publish to anyone
+ self.cmd("progress", ["publish", _["Content publish failed."], -100])
+ else:
+ cbProgress(back, back)
+ return back
# Callback of site publish
def cbSitePublish(self, to, site, thread, notification=True, callback=True):
published = thread.value
if published > 0: # Successfully published
if notification:
- self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000])
+ # self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000])
site.updateWebsocket() # Send updated site data to local websocket clients
if callback:
self.response(to, "ok")
@@ -388,7 +410,6 @@ class UiWebsocket(object):
else:
if notification:
- self.cmd("notification", ["error", _["Content publish failed."]])
self.response(to, {"error": "Content publish failed."})
# Write a file to disk
@@ -448,6 +469,16 @@ class UiWebsocket(object):
):
return self.response(to, {"error": "Forbidden, you can only modify your own files"})
+ file_info = self.site.content_manager.getFileInfo(inner_path)
+ if file_info.get("optional"):
+ self.log.debug("Deleting optional file: %s" % inner_path)
+ relative_path = file_info["relative_path"]
+ content_json = self.site.storage.loadJson(file_info["content_inner_path"])
+ if relative_path in content_json.get("files_optional", {}):
+ del content_json["files_optional"][relative_path]
+ self.site.storage.writeJson(file_info["content_inner_path"], content_json)
+ self.site.content_manager.loadContent(file_info["content_inner_path"], add_bad_files=False, force=True)
+
try:
self.site.storage.delete(inner_path)
except Exception, err:
@@ -468,6 +499,10 @@ class UiWebsocket(object):
# self.log.debug("FileQuery %s %s done in %s" % (dir_inner_path, query, time.time()-s))
return self.response(to, rows)
+ # List files in directory
+ def actionFileList(self, to, inner_path):
+ return self.response(to, list(self.site.storage.list(inner_path)))
+
# Sql query
def actionDbQuery(self, to, query, params=None, wait_for=None):
if config.debug:
@@ -487,14 +522,18 @@ class UiWebsocket(object):
return self.response(to, rows)
# Return file content
- def actionFileGet(self, to, inner_path, required=True):
+ def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300):
try:
if required or inner_path in self.site.bad_files:
- self.site.needFile(inner_path, priority=6)
+ with gevent.Timeout(timeout):
+ self.site.needFile(inner_path, priority=6)
body = self.site.storage.read(inner_path)
except Exception, err:
self.log.debug("%s fileGet error: %s" % (inner_path, err))
body = None
+ if body and format == "base64":
+ import base64
+ body = base64.b64encode(body)
return self.response(to, body)
def actionFileRules(self, to, inner_path):
@@ -514,13 +553,13 @@ class UiWebsocket(object):
if res is True:
self.cmd(
"notification",
- ["done", _("{_[New certificate added:]}
{auth_type}/{auth_user_name}@{domain}.")]
+ ["done", _("{_[New certificate added]:}
{auth_type}/{auth_user_name}@{domain}.")]
)
self.response(to, "ok")
elif res is False:
# Display confirmation of change
cert_current = self.user.certs[domain]
- body = _("{_[You current certificate:]}
{cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}")
+ body = _("{_[Your current certificate]:}
{cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}")
self.cmd(
"confirm",
[body, _("Change it to {auth_type}/{auth_user_name}@{domain}")],
@@ -599,11 +638,13 @@ class UiWebsocket(object):
if permission not in self.site.settings["permissions"]:
self.site.settings["permissions"].append(permission)
self.site.saveSettings()
+ self.site.updateWebsocket(permission_added=permission)
self.response(to, "ok")
def actionPermissionRemove(self, to, permission):
self.site.settings["permissions"].remove(permission)
self.site.saveSettings()
+ self.site.updateWebsocket(permission_removed=permission)
self.response(to, "ok")
# Set certificate that used for authenticate user for site
@@ -681,12 +722,12 @@ class UiWebsocket(object):
else:
self.response(to, {"error": "Unknown site: %s" % address})
- def actionSiteClone(self, to, address):
+ def actionSiteClone(self, to, address, root_inner_path=""):
self.cmd("notification", ["info", "Cloning site..."])
site = self.server.sites.get(address)
# Generate a new site from user's bip32 seed
new_address, new_address_index, new_site_data = self.user.getNewSiteData()
- new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index)
+ new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path)
new_site.settings["own"] = True
new_site.saveSettings()
self.cmd("notification", ["done", _["Site cloned"] + "" % new_address])
diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee
index 1a7f94fa..00c66761 100644
--- a/src/Ui/media/Notifications.coffee
+++ b/src/Ui/media/Notifications.coffee
@@ -21,12 +21,16 @@ class Notifications
# Create element
elem = $(".notification.template", @elem).clone().removeClass("template")
elem.addClass("notification-#{type}").addClass("notification-#{id}")
+ if type == "progress"
+ elem.addClass("notification-done")
# Update text
if type == "error"
$(".notification-icon", elem).html("!")
else if type == "done"
$(".notification-icon", elem).html("
")
+ else if type == "progress"
+ $(".notification-icon", elem).html("
")
else if type == "ask"
$(".notification-icon", elem).html("?")
else
@@ -64,6 +68,8 @@ class Notifications
$(".select", elem).on "click", =>
@close elem
+ return elem
+
close: (elem) ->
elem.stop().animate {"width": 0, "opacity": 0}, 700, "easeInOutCubic"
diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee
index b076d077..927c8f0f 100644
--- a/src/Ui/media/Wrapper.coffee
+++ b/src/Ui/media/Wrapper.coffee
@@ -53,6 +53,8 @@ class Wrapper
if "-" in message.params[0] # - in first param: message id defined
[id, type] = message.params[0].split("-")
@notifications.add(id, type, message.params[1], message.params[2])
+ else if cmd == "progress" # Display notification
+ @actionProgress(message)
else if cmd == "prompt" # Prompt input
@displayPrompt message.params[0], message.params[1], message.params[2], (res) =>
@ws.response message.id, res
@@ -109,6 +111,8 @@ class Wrapper
@actionConfirm(message)
else if cmd == "wrapperPrompt" # Prompt input
@actionPrompt(message)
+ else if cmd == "wrapperProgress" # Progress bar
+ @actionProgress(message)
else if cmd == "wrapperSetViewport" # Set the viewport
@actionSetViewport(message)
else if cmd == "wrapperSetTitle"
@@ -131,6 +135,8 @@ class Wrapper
@actionOpenWindow(message.params)
else if cmd == "wrapperPermissionAdd"
@actionPermissionAdd(message)
+ else if cmd == "wrapperRequestFullscreen"
+ @actionRequestFullscreen()
else # Send to websocket
if message.id < 1000000
@ws.send(message) # Pass message to websocket
@@ -141,7 +147,7 @@ class Wrapper
if query == null
query = window.location.search
back = window.location.pathname
- if back.slice(-1) != "/"
+ if back.match /^\/[^\/]+$/ # Add / after site address if called without it
back += "/"
if query.replace("?", "")
back += "?"+query.replace("?", "")
@@ -168,6 +174,21 @@ class Wrapper
w.opener = null
w.location = params[0]
+ actionRequestFullscreen: ->
+ if "Fullscreen" in @site_info.settings.permissions
+ elem = document.getElementById("inner-iframe")
+ request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen
+ request_fullscreen.call(elem)
+ setTimeout ( =>
+ if window.innerHeight != screen.height # Fullscreen failed, probably only allowed on click
+ @displayConfirm "This site requests permission:" + "
Fullscreen", "Grant", =>
+ request_fullscreen.call(elem)
+ ), 100
+ else
+ @displayConfirm "This site requests permission:" + "
Fullscreen", "Grant", =>
+ @site_info.settings.permissions.push("Fullscreen")
+ @actionRequestFullscreen()
+ @ws.cmd "permissionAdd", "Fullscreen"
actionPermissionAdd: (message) ->
permission = message.params
@@ -180,8 +201,6 @@ class Wrapper
body = $("
"+message.params[1]+"")
@notifications.add("notification-#{message.id}", message.params[0], body, message.params[2])
-
-
displayConfirm: (message, caption, cb) ->
body = $("
"+message+"")
button = $("
#{caption}") # Add confirm button
@@ -232,6 +251,49 @@ class Wrapper
@displayPrompt message.params[0], type, caption, (res) =>
@sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm
+ actionProgress: (message) ->
+ message.params = @toHtmlSafe(message.params) # Escape html
+ percent = Math.min(100, message.params[2])/100
+ offset = 75-(percent*75)
+ circle = """
+
+ """
+ body = "
"+message.params[1]+"" + circle
+ elem = $(".notification-#{message.params[0]}")
+ if elem.length
+ width = $(".body .message", elem).outerWidth()
+ $(".body .message", elem).html(message.params[1])
+ if $(".body .message", elem).css("width") == ""
+ $(".body .message", elem).css("width", width)
+ $(".body .circle-fg", elem).css("stroke-dashoffset", offset)
+ else
+ elem = @notifications.add(message.params[0], "progress", $(body))
+ if percent > 0
+ $(".body .circle-bg", elem).css {"animation-play-state": "paused", "stroke-dasharray": "180px"}
+
+ if $(".notification-icon", elem).data("done")
+ return false
+ else if message.params[2] >= 100 # Done
+ $(".circle-fg", elem).css("transition", "all 0.3s ease-in-out")
+ setTimeout (->
+ $(".notification-icon", elem).css {transform: "scale(1)", opacity: 1}
+ $(".notification-icon .icon-success", elem).css {transform: "rotate(45deg) scale(1)"}
+ ), 300
+ setTimeout (=>
+ @notifications.close elem
+ ), 3000
+ $(".notification-icon", elem).data("done", true)
+ else if message.params[2] < 0 # Error
+ $(".body .circle-fg", elem).css("stroke", "#ec6f47").css("transition", "transition: all 0.3s ease-in-out")
+ setTimeout (=>
+ $(".notification-icon", elem).css {transform: "scale(1)", opacity: 1}
+ elem.removeClass("notification-done").addClass("notification-error")
+ $(".notification-icon .icon-success", elem).removeClass("icon-success").html("!")
+ ), 300
+ $(".notification-icon", elem).data("done", true)
actionSetViewport: (message) ->
@@ -362,7 +424,7 @@ class Wrapper
if site_info.content
window.document.title = site_info.content.title+" - ZeroNet"
@log "Required file done, setting title to", window.document.title
- if not $(".loadingscreen").length # Loading screen already removed (loaded +2sec)
+ if not window.show_loadingscreen
@notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content.")
# File failed downloading
else if site_info.event[0] == "file_failed"
diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css
index 13681a26..4b84d4b2 100644
--- a/src/Ui/media/Wrapper.css
+++ b/src/Ui/media/Wrapper.css
@@ -45,7 +45,7 @@ a { color: black }
color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/
}
.notification-icon {
- display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1;
+ display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2;
text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white;
}
.notification .body {
@@ -82,6 +82,18 @@ a { color: black }
.notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; border-radius: 1px; margin-right: -11px; transition: all 0.3s }
.notification .input:focus { border-color: #95a5a6; outline: none }
+/* Notification progress */
+.notification .circle { width: 50px; height: 50px; position: absolute; left: -50px; top: 0px; background-color: #e2e9ec; z-index: 1; background: linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef); }
+.notification .circle-svg { margin-left: 10px; margin-top: 10px; transform: rotateZ(-90deg); }
+.notification .circle-bg { stroke: #FFF; stroke-width: 2px; animation: rolling 0.4s infinite linear; stroke-dasharray: 40px; transition: all 1s }
+.notification .circle-fg { stroke-dashoffset: 200; stroke: #2ecc71; stroke-width: 2px; stroke-dasharray: 75px; transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); }
+.notification-progress .notification-icon { opacity: 0; transform: scale(0); transition: all 0.3s ease-in-out }
+.notification-progress .icon-success { transform: rotate(45deg) scale(0); transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
+@keyframes rolling {
+ 0% { stroke-dashoffset: 80px }
+ 100% { stroke-dashoffset: 0px }
+}
+
/* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */
.icon-success { left:6px; width:5px; height:12px; border-width:0 5px 5px 0; border-style:solid; border-color:white; margin-left: 20px; margin-top: 15px; transform:rotate(45deg) }
diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css
index 0321f9a4..e960a0b9 100644
--- a/src/Ui/media/all.css
+++ b/src/Ui/media/all.css
@@ -50,7 +50,7 @@ a { color: black }
color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/
}
.notification-icon {
- display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1;
+ display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2;
text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white;
}
.notification .body {
@@ -87,6 +87,27 @@ a { color: black }
.notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; -webkit-border-radius: 1px; -moz-border-radius: 1px; -o-border-radius: 1px; -ms-border-radius: 1px; border-radius: 1px ; margin-right: -11px; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s }
.notification .input:focus { border-color: #95a5a6; outline: none }
+/* Notification progress */
+.notification .circle { width: 50px; height: 50px; position: absolute; left: -50px; top: 0px; background-color: #e2e9ec; z-index: 1; background: -webkit-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -moz-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -o-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -ms-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef); }
+.notification .circle-svg { margin-left: 10px; margin-top: 10px; -webkit-transform: rotateZ(-90deg); -moz-transform: rotateZ(-90deg); -o-transform: rotateZ(-90deg); -ms-transform: rotateZ(-90deg); transform: rotateZ(-90deg) ; }
+.notification .circle-bg { stroke: #FFF; stroke-width: 2px; -webkit-animation: rolling 0.4s infinite linear; -moz-animation: rolling 0.4s infinite linear; -o-animation: rolling 0.4s infinite linear; -ms-animation: rolling 0.4s infinite linear; animation: rolling 0.4s infinite linear ; stroke-dasharray: 40px; -webkit-transition: all 1s ; -moz-transition: all 1s ; -o-transition: all 1s ; -ms-transition: all 1s ; transition: all 1s }
+.notification .circle-fg { stroke-dashoffset: 200; stroke: #2ecc71; stroke-width: 2px; stroke-dasharray: 75px; -webkit-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -moz-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -o-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -ms-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); transition: all 5s cubic-bezier(0.19, 1, 0.22, 1) ; }
+.notification-progress .notification-icon { opacity: 0; -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0) ; -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 }
+.notification-progress .icon-success { -webkit-transform: rotate(45deg) scale(0); -moz-transform: rotate(45deg) scale(0); -o-transform: rotate(45deg) scale(0); -ms-transform: rotate(45deg) scale(0); transform: rotate(45deg) scale(0) ; -webkit-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -moz-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -o-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -ms-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) ; }
+@keyframes rolling {
+ 0% { stroke-dashoffset: 80px }
+ 100% { stroke-dashoffset: 0px }
+}
+@-webkit-keyframes rolling {
+ 0% { stroke-dashoffset: 80px }
+ 100% { stroke-dashoffset: 0px }
+}
+@-moz-keyframes rolling {
+ 0% { stroke-dashoffset: 80px }
+ 100% { stroke-dashoffset: 0px }
+}
+
+
/* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */
.icon-success { left:6px; width:5px; height:12px; border-width:0 5px 5px 0; border-style:solid; border-color:white; margin-left: 20px; margin-top: 15px; transform:rotate(45deg) }
diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js
index 38149779..f597fe02 100644
--- a/src/Ui/media/all.js
+++ b/src/Ui/media/all.js
@@ -688,10 +688,15 @@ jQuery.extend( jQuery.easing,
}
elem = $(".notification.template", this.elem).clone().removeClass("template");
elem.addClass("notification-" + type).addClass("notification-" + id);
+ if (type === "progress") {
+ elem.addClass("notification-done");
+ }
if (type === "error") {
$(".notification-icon", elem).html("!");
} else if (type === "done") {
$(".notification-icon", elem).html("
");
+ } else if (type === "progress") {
+ $(".notification-icon", elem).html("
");
} else if (type === "ask") {
$(".notification-icon", elem).html("?");
} else {
@@ -735,11 +740,12 @@ jQuery.extend( jQuery.easing,
return false;
};
})(this));
- return $(".select", elem).on("click", (function(_this) {
+ $(".select", elem).on("click", (function(_this) {
return function() {
return _this.close(elem);
};
})(this));
+ return elem;
};
Notifications.prototype.close = function(elem) {
@@ -848,6 +854,8 @@ jQuery.extend( jQuery.easing,
_ref = message.params[0].split("-"), id = _ref[0], type = _ref[1];
}
return this.notifications.add(id, type, message.params[1], message.params[2]);
+ } else if (cmd === "progress") {
+ return this.actionProgress(message);
} else if (cmd === "prompt") {
return this.displayPrompt(message.params[0], message.params[1], message.params[2], (function(_this) {
return function(res) {
@@ -915,6 +923,8 @@ jQuery.extend( jQuery.easing,
return this.actionConfirm(message);
} else if (cmd === "wrapperPrompt") {
return this.actionPrompt(message);
+ } else if (cmd === "wrapperProgress") {
+ return this.actionProgress(message);
} else if (cmd === "wrapperSetViewport") {
return this.actionSetViewport(message);
} else if (cmd === "wrapperSetTitle") {
@@ -941,6 +951,8 @@ jQuery.extend( jQuery.easing,
return this.actionOpenWindow(message.params);
} else if (cmd === "wrapperPermissionAdd") {
return this.actionPermissionAdd(message);
+ } else if (cmd === "wrapperRequestFullscreen") {
+ return this.actionRequestFullscreen();
} else {
if (message.id < 1000000) {
return this.ws.send(message);
@@ -959,7 +971,7 @@ jQuery.extend( jQuery.easing,
query = window.location.search;
}
back = window.location.pathname;
- if (back.slice(-1) !== "/") {
+ if (back.match(/^\/[^\/]+$/)) {
back += "/";
}
if (query.replace("?", "")) {
@@ -992,6 +1004,32 @@ jQuery.extend( jQuery.easing,
}
};
+ Wrapper.prototype.actionRequestFullscreen = function() {
+ var elem, request_fullscreen;
+ if (__indexOf.call(this.site_info.settings.permissions, "Fullscreen") >= 0) {
+ elem = document.getElementById("inner-iframe");
+ request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen;
+ request_fullscreen.call(elem);
+ return setTimeout(((function(_this) {
+ return function() {
+ if (window.innerHeight !== screen.height) {
+ return _this.displayConfirm("This site requests permission:" + "
Fullscreen", "Grant", function() {
+ return request_fullscreen.call(elem);
+ });
+ }
+ };
+ })(this)), 100);
+ } else {
+ return this.displayConfirm("This site requests permission:" + "
Fullscreen", "Grant", (function(_this) {
+ return function() {
+ _this.site_info.settings.permissions.push("Fullscreen");
+ _this.actionRequestFullscreen();
+ return _this.ws.cmd("permissionAdd", "Fullscreen");
+ };
+ })(this));
+ }
+ };
+
Wrapper.prototype.actionPermissionAdd = function(message) {
var permission;
permission = message.params;
@@ -1099,6 +1137,65 @@ jQuery.extend( jQuery.easing,
})(this));
};
+ Wrapper.prototype.actionProgress = function(message) {
+ var body, circle, elem, offset, percent, width;
+ message.params = this.toHtmlSafe(message.params);
+ percent = Math.min(100, message.params[2]) / 100;
+ offset = 75 - (percent * 75);
+ circle = "
";
+ body = "
" + message.params[1] + "" + circle;
+ elem = $(".notification-" + message.params[0]);
+ if (elem.length) {
+ width = $(".body .message", elem).outerWidth();
+ $(".body .message", elem).html(message.params[1]);
+ if ($(".body .message", elem).css("width") === "") {
+ $(".body .message", elem).css("width", width);
+ }
+ $(".body .circle-fg", elem).css("stroke-dashoffset", offset);
+ } else {
+ elem = this.notifications.add(message.params[0], "progress", $(body));
+ }
+ if (percent > 0) {
+ $(".body .circle-bg", elem).css({
+ "animation-play-state": "paused",
+ "stroke-dasharray": "180px"
+ });
+ }
+ if ($(".notification-icon", elem).data("done")) {
+ return false;
+ } else if (message.params[2] >= 100) {
+ $(".circle-fg", elem).css("transition", "all 0.3s ease-in-out");
+ setTimeout((function() {
+ $(".notification-icon", elem).css({
+ transform: "scale(1)",
+ opacity: 1
+ });
+ return $(".notification-icon .icon-success", elem).css({
+ transform: "rotate(45deg) scale(1)"
+ });
+ }), 300);
+ setTimeout(((function(_this) {
+ return function() {
+ return _this.notifications.close(elem);
+ };
+ })(this)), 3000);
+ return $(".notification-icon", elem).data("done", true);
+ } else if (message.params[2] < 0) {
+ $(".body .circle-fg", elem).css("stroke", "#ec6f47").css("transition", "transition: all 0.3s ease-in-out");
+ setTimeout(((function(_this) {
+ return function() {
+ $(".notification-icon", elem).css({
+ transform: "scale(1)",
+ opacity: 1
+ });
+ elem.removeClass("notification-done").addClass("notification-error");
+ return $(".notification-icon .icon-success", elem).removeClass("icon-success").html("!");
+ };
+ })(this)), 300);
+ return $(".notification-icon", elem).data("done", true);
+ }
+ };
+
Wrapper.prototype.actionSetViewport = function(message) {
this.log("actionSetViewport", message);
if ($("#viewport").length > 0) {
@@ -1273,7 +1370,7 @@ jQuery.extend( jQuery.easing,
window.document.title = site_info.content.title + " - ZeroNet";
this.log("Required file done, setting title to", window.document.title);
}
- if (!$(".loadingscreen").length) {
+ if (!window.show_loadingscreen) {
this.notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content.");
}
}
diff --git a/src/lib/opensslVerify/HashInfo.txt b/src/lib/opensslVerify/HashInfo.txt
index f5308e27..1fe6e619 100644
Binary files a/src/lib/opensslVerify/HashInfo.txt and b/src/lib/opensslVerify/HashInfo.txt differ
diff --git a/src/lib/opensslVerify/OpenSSL License.txt b/src/lib/opensslVerify/OpenSSL License.txt
index 97234459..3090896c 100644
--- a/src/lib/opensslVerify/OpenSSL License.txt
+++ b/src/lib/opensslVerify/OpenSSL License.txt
@@ -12,7 +12,7 @@
---------------
/* ====================================================================
- * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved.
+ * Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
diff --git a/src/lib/opensslVerify/ReadMe.txt b/src/lib/opensslVerify/ReadMe.txt
index 352ccef6..beaf580e 100644
--- a/src/lib/opensslVerify/ReadMe.txt
+++ b/src/lib/opensslVerify/ReadMe.txt
@@ -1,10 +1,10 @@
=============================================================================
-OpenSSL v1.0.2a Precompiled Binaries for Win32
+OpenSSL v1.0.2j Precompiled Binaries for Win32
-----------------------------------------------------------------------------
*** Release Information ***
-Release Date: Mrz 20, 2015
+Release Date: Okt 01, 2016
Author: Frederik A. Winkelsdorf (opendec.wordpress.com)
for the Indy Project (www.indyproject.org)
@@ -15,7 +15,7 @@ Dependencies: The libraries have no noteworthy dependencies
Installation: Copy both DLL files into your application directory
-Supported OS: Windows 2000 up to Windows 8
+Supported OS: Windows 2000 up to Windows 10
-----------------------------------------------------------------------------
@@ -42,10 +42,10 @@ SOFTWARE AND/OR PATENTS.
*** Build Information Win32 ***
Built with: Microsoft Visual C++ 2008 Express Edition
- The Netwide Assembler (NASM) v2.11.05 Win32
- Strawberry Perl v5.20.0.1 Win32 Portable
+ The Netwide Assembler (NASM) v2.11.08 Win32
+ Strawberry Perl v5.22.0.1 Win32 Portable
Windows PowerShell
- FinalBuilder 7 Embarcadero Edition
+ FinalBuilder 7
Commands: perl configure VC-WIN32
ms\do_nasm
diff --git a/src/lib/opensslVerify/libeay32.dll b/src/lib/opensslVerify/libeay32.dll
index 6359cc5a..dd7b6379 100644
Binary files a/src/lib/opensslVerify/libeay32.dll and b/src/lib/opensslVerify/libeay32.dll differ
diff --git a/src/lib/opensslVerify/openssl.exe b/src/lib/opensslVerify/openssl.exe
index 1f5127e5..0bcd58b0 100644
Binary files a/src/lib/opensslVerify/openssl.exe and b/src/lib/opensslVerify/openssl.exe differ
diff --git a/src/lib/opensslVerify/opensslVerify.py b/src/lib/opensslVerify/opensslVerify.py
index 8103bea0..8c9bffe7 100644
--- a/src/lib/opensslVerify/opensslVerify.py
+++ b/src/lib/opensslVerify/opensslVerify.py
@@ -196,11 +196,13 @@ def openLibrary():
global ssl
try:
if sys.platform.startswith("win"):
- dll_path = "src/lib/opensslVerify/libeay32.dll"
+ dll_path = os.path.dirname(os.path.abspath(__file__)) + "/" + "libeay32.dll"
elif sys.platform == "cygwin":
dll_path = "/bin/cygcrypto-1.0.0.dll"
- elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle
+ elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX
dll_path = "../lib/libcrypto.so"
+ elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware
+ dll_path = "/opt/lib/libcrypto.so.1.0.0"
else:
dll_path = "/usr/local/ssl/lib/libcrypto.so"
ssl = _OpenSSL(dll_path)
@@ -456,7 +458,7 @@ if __name__ == "__main__":
sign = btctools.ecdsa_sign("hello", priv) # HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ=
s = time.time()
- for i in range(100):
+ for i in range(1000):
pubkey = getMessagePubkey("hello", sign)
verified = btctools.pubkey_to_address(pubkey) == address
print "100x Verified", verified, time.time() - s
diff --git a/src/lib/opensslVerify/ssleay32.dll b/src/lib/opensslVerify/ssleay32.dll
index b8b86115..8f57f6b3 100644
Binary files a/src/lib/opensslVerify/ssleay32.dll and b/src/lib/opensslVerify/ssleay32.dll differ
diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py
index 12953788..694451ab 100644
--- a/src/lib/pyelliptic/openssl.py
+++ b/src/lib/pyelliptic/openssl.py
@@ -434,10 +434,10 @@ def openLibrary():
global OpenSSL
try:
if sys.platform.startswith("win"):
- dll_path = "src/lib/opensslVerify/libeay32.dll"
+ dll_path = os.path.normpath(os.path.dirname(__file__) + "/../opensslVerify/" + "libeay32.dll")
elif sys.platform == "cygwin":
dll_path = "/bin/cygcrypto-1.0.0.dll"
- elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle
+ elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX
dll_path = "../lib/libcrypto.so"
else:
dll_path = "/usr/local/ssl/lib/libcrypto.so"
diff --git a/src/main.py b/src/main.py
index 220599e5..5f894271 100644
--- a/src/main.py
+++ b/src/main.py
@@ -30,14 +30,22 @@ if not config.arguments: # Config parse failed, show the help screen and exit
# Create necessary files and dirs
if not os.path.isdir(config.log_dir):
os.mkdir(config.log_dir)
+ try:
+ os.chmod(config.log_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+ except Exception, err:
+ print "Can't change permission of %s: %s" % (config.log_dir, err)
+
if not os.path.isdir(config.data_dir):
os.mkdir(config.data_dir)
+ try:
+ os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+ except Exception, err:
+ print "Can't change permission of %s: %s" % (config.data_dir, err)
+
if not os.path.isfile("%s/sites.json" % config.data_dir):
open("%s/sites.json" % config.data_dir, "w").write("{}")
- os.chmod("%s/sites.json" % config.data_dir, stat.S_IRUSR | stat.S_IWUSR)
if not os.path.isfile("%s/users.json" % config.data_dir):
open("%s/users.json" % config.data_dir, "w").write("{}")
- os.chmod("%s/users.json" % config.data_dir, stat.S_IRUSR | stat.S_IWUSR)
# Setup logging
if config.action == "main":
@@ -180,7 +188,7 @@ class Actions(object):
logging.info("Site created!")
- def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False):
+ def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False):
from Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
@@ -200,7 +208,7 @@ class Actions(object):
import getpass
privatekey = getpass.getpass("Private key (input hidden):")
diffs = site.content_manager.getDiffs(inner_path)
- succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True)
+ succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional)
if succ and publish:
self.sitePublish(address, inner_path=inner_path, diffs=diffs)
diff --git a/src/util/Platform.py b/src/util/Platform.py
index f7b07e51..19477649 100644
--- a/src/util/Platform.py
+++ b/src/util/Platform.py
@@ -15,8 +15,8 @@ def setMaxfilesopened(limit):
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if soft < limit:
- logging.debug("Current RLIMIT_NOFILE: %s, changing to %s..." % (soft, limit))
- resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
+ logging.debug("Current RLIMIT_NOFILE: %s (max: %s), changing to %s..." % (soft, hard, limit))
+ resource.setrlimit(resource.RLIMIT_NOFILE, (limit, hard))
return True
except Exception, err:
diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py
index 1e47d6a3..b2a163ae 100644
--- a/src/util/UpnpPunch.py
+++ b/src/util/UpnpPunch.py
@@ -4,6 +4,7 @@ import httplib
import logging
from urlparse import urlparse
from xml.dom.minidom import parseString
+from xml.parsers.expat import ExpatError
from gevent import socket
@@ -82,7 +83,7 @@ def _retrieve_igd_profile(url):
Retrieve the device's UPnP profile.
"""
try:
- return urllib2.urlopen(url.geturl(), timeout=5).read()
+ return urllib2.urlopen(url.geturl(), timeout=5).read().decode('utf-8')
except socket.error:
raise IGDError('IGD profile query timed out')
@@ -100,7 +101,11 @@ def _parse_igd_profile(profile_xml):
WANIPConnection or WANPPPConnection and return
the 'controlURL' and the service xml schema.
"""
- dom = parseString(profile_xml)
+ try:
+ dom = parseString(profile_xml)
+ except ExpatError as e:
+ raise IGDError(
+ 'Unable to parse IGD reply: {0} \n\n\n {1}'.format(profile_xml, e))
service_types = dom.getElementsByTagName('serviceType')
for service in service_types:
@@ -268,7 +273,7 @@ def _send_requests(messages, location, upnp_schema, control_path):
raise UpnpError('Sending requests using UPnP failed.')
-def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=['TCP', 'UDP']):
+def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=("TCP", "UDP")):
logging.debug("Trying using local ip: %s" % ip)
idg_data = _collect_idg_data(ip)
@@ -284,7 +289,7 @@ def _communicate_with_igd(port=15441,
desc="UpnpPunch",
retries=3,
fn=_create_open_message,
- protos=["TCP", "UDP"]):
+ protos=("TCP", "UDP")):
"""
Manage sending a message generated by 'fn'.
"""
@@ -310,7 +315,7 @@ def _communicate_with_igd(port=15441,
port, retries))
-def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UDP"]):
+def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")):
logging.debug("Trying to open port %d." % port)
_communicate_with_igd(port=port,
desc=desc,
@@ -319,7 +324,7 @@ def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UD
protos=protos)
-def ask_to_close_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UDP"]):
+def ask_to_close_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")):
logging.debug("Trying to close port %d." % port)
# retries=1 because multiple successes cause 500 response and failure
_communicate_with_igd(port=port,
diff --git a/src/util/helper.py b/src/util/helper.py
index 13439cde..597a8d75 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -14,17 +14,15 @@ from Config import config
def atomicWrite(dest, content, mode="w"):
try:
- permissions = stat.S_IMODE(os.lstat(dest).st_mode)
- with open(dest + "-new", mode) as f:
+ with open(dest + "-tmpnew", mode) as f:
f.write(content)
f.flush()
os.fsync(f.fileno())
- if os.path.isfile(dest + "-old"): # Previous incomplete write
- os.rename(dest + "-old", dest + "-old-%s" % time.time())
- os.rename(dest, dest + "-old")
- os.rename(dest + "-new", dest)
- os.chmod(dest, permissions)
- os.unlink(dest + "-old")
+ if os.path.isfile(dest + "-tmpold"): # Previous incomplete write
+ os.rename(dest + "-tmpold", dest + "-tmpold-%s" % time.time())
+ os.rename(dest, dest + "-tmpold")
+ os.rename(dest + "-tmpnew", dest)
+ os.unlink(dest + "-tmpold")
return True
except Exception, err:
from Debug import Debug
@@ -32,8 +30,8 @@ def atomicWrite(dest, content, mode="w"):
"File %s write failed: %s, reverting..." %
(dest, Debug.formatException(err))
)
- if os.path.isfile(dest + "-old") and not os.path.isfile(dest):
- os.rename(dest + "-old", dest)
+ if os.path.isfile(dest + "-tmpold") and not os.path.isfile(dest):
+ os.rename(dest + "-tmpold", dest)
return False
@@ -54,7 +52,7 @@ def openLocked(path, mode="w"):
def getFreeSpace():
free_space = -1
if "statvfs" in dir(os): # Unix
- statvfs = os.statvfs(config.data_dir)
+ statvfs = os.statvfs(config.data_dir.encode("utf8"))
free_space = statvfs.f_frsize * statvfs.f_bavail
else: # Windows
try:
diff --git a/update.py b/update.py
index 1ca15a1a..bfdeb50c 100644
--- a/update.py
+++ b/update.py
@@ -1,6 +1,7 @@
import urllib
import zipfile
import os
+import sys
import ssl
import httplib
import socket
@@ -52,6 +53,11 @@ def download():
def update():
from Config import config
+ if getattr(sys, 'source_update_dir', False):
+ if not os.path.isdir(sys.source_update_dir):
+ os.makedirs(sys.source_update_dir)
+ os.chdir(sys.source_update_dir) # New source code will be stored in different directory
+
updatesite_path = config.data_dir + "/" + config.updatesite
sites_json = json.load(open(config.data_dir + "/sites.json"))
updatesite_bad_files = sites_json.get(config.updatesite, {}).get("cache", {}).get("bad_files", {})
@@ -79,7 +85,7 @@ def update():
plugins_enabled.append(dir)
print "Plugins enabled:", plugins_enabled, "disabled:", plugins_disabled
- print "Extracting...",
+ print "Extracting to %s..." % os.getcwd(),
for inner_path in inner_paths:
if ".." in inner_path:
continue
diff --git a/zeronet.py b/zeronet.py
index f56161ee..8fd07968 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -45,11 +45,16 @@ def main():
handler.close()
logger.removeHandler(handler)
-
- except (Exception, ): # Prevent closing
+ except Exception, err: # Prevent closing
import traceback
- traceback.print_exc()
- traceback.print_exc(file=open("log/error.log", "a"))
+ try:
+ import logging
+ logging.exception("Unhandled exception: %s" % err)
+ except Exception, log_err:
+ print "Failed to log error:", log_err
+ traceback.print_exc()
+ from Config import config
+ traceback.print_exc(file=open(config.log_dir + "/error.log", "a"))
if main and main.update_after_shutdown: # Updater
# Restart
@@ -58,11 +63,22 @@ def main():
import time
time.sleep(1) # Wait files to close
args = sys.argv[:]
- args.insert(0, sys.executable)
+
+ sys.executable = sys.executable.replace(".pkg", "") # Frozen mac fix
+
+ if not getattr(sys, 'frozen', False):
+ args.insert(0, sys.executable)
+
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args]
- os.execv(sys.executable, args)
+
+ try:
+ print "Executing %s %s" % (sys.executable, args)
+ os.execv(sys.executable, args)
+ except Exception, err:
+ print "Execv error: %s" % err
print "Bye."
+
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()