Просмотр исходного кода

Merge remote-tracking branch 'PrivateBin/master' into Italian-Translation

Stefano Martinelli 9 лет назад
Родитель
Сommit
2ffc42eb8d

+ 1 - 0
.gitignore

@@ -31,6 +31,7 @@ vendor/**/build_phar.php
 # Ignore local node modules, unit testing logs, api docs and eclipse project files
 js/node_modules/
 tst/log/
+tst/ConfigurationCombinationsTest.php
 .settings
 .buildpath
 .project

+ 14 - 3
.travis.yml

@@ -1,15 +1,26 @@
 language: php
 sudo: false
 php:
-  - 5.5
-  - 5.6
-  - 7.0
+  - '5.4'
+  - '5.5'
+  - '5.6'
+  - '7.0'
+  - '7.1'
+
+# as this is a php project, node.js v4 (for JS unit testing) isn't installed
+install:
+  - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 4
 
 before_script:
   - composer install -n
+  - npm install -g mocha
+  - cd js
+  - npm install jsverify jsdom jsdom-global
+  - cd ..
 
 script:
   - cd tst && ../vendor/bin/phpunit
+  - cd ../js && mocha
 
 after_script:
   - cd ..

+ 1 - 1
INSTALL.md

@@ -10,7 +10,7 @@ check the options and adjust them as you see fit.
 
 ### Requirements
 
-- PHP version 5.3 or above
+- PHP version 5.4 or above
 - _one_ of the following sources of cryptographically safe randomness is required:
   - PHP 7 or higher
   - [Libsodium](https://download.libsodium.org/libsodium/content/installation/) and it's [PHP extension](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium)

+ 1 - 1
composer.json

@@ -18,7 +18,7 @@
         }
     ],
     "require": {
-        "php": "^5.3.0 || ^7.0",
+        "php": "^5.4.0 || ^7.0",
         "paragonie/random_compat": "2.0.4",
         "yzalis/identicon": "1.1.0"
     },

+ 3 - 4
i18n/de.json

@@ -7,8 +7,8 @@
     "en": "de",
     "Paste does not exist, has expired or has been deleted.":
         "Diesen Text gibt es nicht, er ist abgelaufen oder wurde gelöscht.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "%s benötigt PHP 5.3.0 oder höher, um zu funktionieren. Sorry.",
+    "%s requires php %s or above to work. Sorry.":
+        "%s benötigt PHP %s oder höher, um zu funktionieren. Sorry.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s benötigt den Konfigurationsabschnitt [%s] in der Konfigurationsdatei um zu funktionieren.",
     "Please wait %d seconds between each post.":
@@ -151,6 +151,5 @@
     "Preparing new paste…": "Bereite neues Paste vor…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
         "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">die FAQ</a> (englisch), um zu sehen, wie der Fehler behoben werden kann.",
-    "+++ no paste text +++":
-        "+++ kein Paste-Text +++"
+    "+++ no paste text +++": "+++ kein Paste-Text +++"
 }

+ 11 - 7
i18n/es.json

@@ -7,8 +7,8 @@
     "en": "es",
     "Paste does not exist, has expired or has been deleted.":
         "El texto no existe, ha caducado o ha sido eliminado.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "%s requiere php 5.3.0 o superior para funcionar. Lo siento.",
+    "%s requires php %s or above to work. Sorry.":
+        "%s requiere php %s o superior para funcionar. Lo siento.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s requiere que la sección de configuración [%s] esté presente en el archivo de configuración.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Responder",
     "Anonymous":
         "Anónimo",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Avatar anónimo (Vizhash de la dirección IP)",
     "Add comment":
         "Añadir comentario",
@@ -112,8 +112,8 @@
         "Error del servidor o el servidor no responde",
     "Could not post comment: %s":
         "No fue posible publicar comentario: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Enviando texto (Por favor, mueva el ratón para mayor entropía)…",
+    "Please move your mouse for more entropy…":
+        "Por favor, mueva el ratón para mayor entropía…",
     "Sending paste…":
         "Enviando texto…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -129,7 +129,8 @@
     "Source Code": "Código fuente",
     "Markdown": "Markdown",
     "Download attachment": "Descargar adjunto",
-    "Cloned file attached.": "Archivo clonado adjunto.",
+    "Cloned: '%s'": "Clonado: '%s'.",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Adjuntar archivo",
     "Remove attachment": "Remover adjunto",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "Ingrese contraseña",
     "Loading…": "Cargando…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "En caso de que este mensaje nunca desaparezca por favor revise <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para obtener información para solucionar problemas</a>."
+        "En caso de que este mensaje nunca desaparezca por favor revise <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para obtener información para solucionar problemas</a>.",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 11 - 7
i18n/fr.json

@@ -7,8 +7,8 @@
     "en": "fr",
     "Paste does not exist, has expired or has been deleted.":
         "Le paste n'existe pas, a expiré, ou a été supprimé.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "Désolé, %s nécessite php 5.3.0 ou supérieur pour fonctionner.",
+    "%s requires php %s or above to work. Sorry.":
+        "Désolé, %s nécessite php %s ou supérieur pour fonctionner.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Répondre",
     "Anonymous":
         "Anonyme",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Avatar anonyme (Vizhash de l'adresse IP)",
     "Add comment":
         "Ajouter un commentaire",
@@ -112,8 +112,8 @@
         "Le serveur ne répond pas ou a rencontré une erreur",
     "Could not post comment: %s":
         "Impossible de poster le commentaire : %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Envoi du paste (Merci de bouger votre souris pour plus d'entropie)…",
+    "Please move your mouse for more entropy…":
+        "Merci de bouger votre souris pour plus d'entropie…",
     "Sending paste…":
         "Envoi du paste…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -138,7 +138,8 @@
     "Source Code": "Code source",
     "Markdown": "Markdown",
     "Download attachment": "Télécharger la pièce jointe",
-    "Cloned file attached.": "Cloner le fichier attaché.",
+    "Cloned: '%s'": "Cloner '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Attacher un fichier ",
     "Remove attachment": "Enlever l'attachement",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -155,6 +156,9 @@
     "Enter password":
         "Entrez le mot de passe",
     "Loading…": "Chargement…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "Si ce message ne disparaîssait pas, jetez un oeil à <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">cette FAQ pour des idées de résolution</a> (en Anglais)."
+        "Si ce message ne disparaîssait pas, jetez un oeil à <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">cette FAQ pour des idées de résolution</a> (en Anglais).",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 11 - 7
i18n/it.json

@@ -7,8 +7,8 @@
     "en": "it",
     "Paste does not exist, has expired or has been deleted.":
         "Questo messaggio non esiste, è scaduto o è stato cancellato.",
-    "%s requires php 5.4.0 or above to work. Sorry.":
-        "%s richiede php 5.4.0 o superiore per funzionare. Ci spiace.",
+    "%s requires php %s or above to work. Sorry.":
+        "%s richiede php %s o superiore per funzionare. Ci spiace.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s richiede la presenza della sezione [%s] nei file di configurazione.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Rispondi",
     "Anonymous":
         "Anonimo",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Avatar Anonimo (Vizhash dell'indirizzo IP)",
     "Add comment":
         "Aggiungi un commento",
@@ -112,8 +112,8 @@
         "errore o mancata risposta dal server",
     "Could not post comment: %s":
         "Impossibile inviare il commento: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Invio messaggio (Muovi il mouse in modo casuale, per generare maggior entropia)…",
+    "Please move your mouse for more entropy…":
+        "Muovi il mouse in modo casuale, per generare maggior entropia…",
     "Sending paste…":
         "Messaggio in fase di invio…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -129,7 +129,8 @@
     "Source Code": "Codice Sorgente",
     "Markdown": "Markdown",
     "Download attachment": "Scarica Allegato",
-    "Cloned file attached.": "Copia del file allegata.",
+    "Cloned: '%s'": "Copia: '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Allega un file",
     "Remove attachment": "Rimuovi allegato",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "Inserisci la password",
     "Loading…": "Loading…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "Nel caso questo messaggio non scompaia, controlla questa <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\"> FAQ</a> per trovare informazioni su come risolvere il problema (in Inglese)."
+        "Nel caso questo messaggio non scompaia, controlla questa <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\"> FAQ</a> per trovare informazioni su come risolvere il problema (in Inglese).",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 11 - 7
i18n/no.json

@@ -7,8 +7,8 @@
     "en": "no",
     "Paste does not exist, has expired or has been deleted.":
         "Innlegget eksisterer ikke, er utløpt eller har blitt slettet.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "Beklager, %s krever php 5.3.0 eller nyere for å kjøre.",
+    "%s requires php %s or above to work. Sorry.":
+        "Beklager, %s krever php %s eller nyere for å kjøre.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s krever konfigurasjonsdel [%s] å være til stede i konfigurasjonsfilen .",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Svar",
     "Anonymous":
         "Anonym",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Anonym avatar (Vizhash av IP adressen)",
     "Add comment":
         "Legg til kommentar",
@@ -112,8 +112,8 @@
         "server feilet eller svarer ikke",
     "Could not post comment: %s":
         "Kunne ikke sende kommentar: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Sender innlegg (Flytt musen for mere entropi)…",
+    "Please move your mouse for more entropy…":
+        "Flytt musen for mere entropi…",
     "Sending paste…":
         "Sender innlegg…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -129,7 +129,8 @@
     "Source Code": "Kildekode",
     "Markdown": "Oppmerket",
     "Download attachment": "Last ned vedlegg",
-    "Cloned file attached.": "Kopier vedlegg.",
+    "Cloned: '%s'": "Kopier: '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Legg til fil",
     "Remove attachment": "Slett vedlegg",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "Skriv inn passord",
     "Loading…": "Laster…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">ofte stilte spørsmål</a> for informasjon om feilsøking."
+        "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">ofte stilte spørsmål</a> for informasjon om feilsøking.",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 11 - 7
i18n/oc.json

@@ -7,8 +7,8 @@
     "en": "oc",
     "Paste does not exist, has expired or has been deleted.":
         "Lo tèxte existís pas, a expirat, o es estat suprimit.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "O planhèm, %s necessita php 5.3.0 o superior per foncionar.",
+    "%s requires php %s or above to work. Sorry.":
+        "O planhèm, %s necessita php %s o superior per foncionar.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s fa besonh de la seccion de configuracion [%s] dins lo fichièr de configuracion per foncionar.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Respondre",
     "Anonymous":
         "Anonime",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Avatar anonime (Vizhash de l'adreça IP)",
     "Add comment":
         "Apondre un comentari",
@@ -112,8 +112,8 @@
         "Lo servidor respond pas o a rencontrat una error",
     "Could not post comment: %s":
         "Impossible de mandar lo comentari : %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Mandadís del tèxte (Mercés de bolegar vòstra mirga per mai entropia)…",
+    "Please move your mouse for more entropy…":
+        "Mercés de bolegar vòstra mirga per mai entropia…",
     "Sending paste…":
         "Mandadís del tèxte…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -138,7 +138,8 @@
     "Source Code": "Còdi font",
     "Markdown": "Markdown",
     "Download attachment": "Telecargar la pèça junta",
-    "Cloned file attached.": "Clonar lo fichièr junt.",
+    "Cloned: '%s'": "Clonar: '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Juntar un fichièr ",
     "Remove attachment": "Levar la pèca junta",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -155,6 +156,9 @@
     "Enter password":
         "Picatz lo senhal",
     "Loading…": "Cargament…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">aquesta FAQ per las solucions</a> (en Anglés)."
+        "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">aquesta FAQ per las solucions</a> (en Anglés).",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 11 - 7
i18n/pl.json

@@ -7,8 +7,8 @@
     "en": "pl",
     "Paste does not exist, has expired or has been deleted.":
         "Wklejka nie istnieje, wygasła albo została usunięta.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "%s wymaga PHP w wersji 5.3.0 lub nowszej, sorry.",
+    "%s requires php %s or above to work. Sorry.":
+        "%s wymaga PHP w wersji %s lub nowszej, sorry.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s wymaga obecności sekcji [%s] w pliku konfiguracyjnym.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Odpowiedz",
     "Anonymous":
         "Anonim",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Anonimowy avatar (Vizhash z adresu IP)",
     "Add comment":
         "Dodaj komentarz",
@@ -112,8 +112,8 @@
         "bląd serwera lub brak odpowiedzi",
     "Could not post comment: %s":
         "Nie udało się wysłać komentarza: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Wysyłanie wklejki (proszę poruszać myszą aby uzyskać większą entropię)…",
+    "Please move your mouse for more entropy…":
+        "Proszę poruszać myszą aby uzyskać większą entropię…",
     "Sending paste…":
         "Wysyłanie wklejki…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -129,7 +129,8 @@
     "Source Code": "Kod źródłowy",
     "Markdown": "Markdown",
     "Download attachment": "Pobierz załącznik",
-    "Cloned file attached.": "Sklonowano załączony plik.",
+    "Cloned: '%s'": "Sklonowano: '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Załącz plik",
     "Remove attachment": "Usuń załącznik",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "Wpisz hasło",
     "Loading…": "Loading…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English)."
+        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English).",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 8 - 4
i18n/pt.json

@@ -7,8 +7,8 @@
     "en": "pt",
     "Paste does not exist, has expired or has been deleted.":
         "A cópia não existe, expirou ou já foi excluída.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "%s requer php 5.3.0 ou superior para funcionar. Desculpa.",
+    "%s requires php %s or above to work. Sorry.":
+        "%s requer php %s ou superior para funcionar. Desculpa.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s requer que a seção de configuração [% s] esteja no arquivo de configuração.",
     "Please wait %d seconds between each post.":
@@ -93,7 +93,7 @@
     "Anonymous":
         "Anônimo",
     "Avatar generated from IP address":
-        "Avatar (do endereço IP)",
+        "Avatar gerado à partir do endereço IP",
     "Add comment":
         "Adicionar comentário",
     "Optional nickname…":
@@ -130,6 +130,7 @@
     "Markdown": "Markdown",
     "Download attachment": "Baixar anexo",
     "Cloned: '%s'": "Clonado: '%s'",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Anexar um arquivo",
     "Remove attachment": "Remover anexo",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "Digite a senha",
     "Loading…": "Carregando…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "Caso essa mensagem nunca desapareça, por favor veja <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para saber como resolver os problemas</a>."
+        "Caso essa mensagem nunca desapareça, por favor veja <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para saber como resolver os problemas</a>.",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 16 - 10
i18n/ru.json

@@ -7,8 +7,8 @@
     "en": "ru",
     "Paste does not exist, has expired or has been deleted.":
         "Запись не существует, просрочена или была удалена.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "Для работы %s требуется PHP 5.3.0 или выше. Извините.",
+    "%s requires php %s or above to work. Sorry.":
+        "Для работы %s требуется php %s или выше. Извините.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s необходимо наличие секции [%s] в конфигурационном файле.",
     "Please wait %d seconds between each post.":
@@ -32,7 +32,7 @@
     "Paste was properly deleted.":
         "Запись была успешно удалена.",
     "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Для работы %s требуется включенный JavaScript.<br />Приносим извинения за неудобства..",
+        "Для работы %s требуется включенный JavaScript.<br />Приносим извинения за неудобства.",
     "%s requires a modern browser to work.":
         "Для работы %s требуется более современный браузер.",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
@@ -92,7 +92,7 @@
         "Ответить",
     "Anonymous":
         "Аноним",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Анонимный аватар (Vizhash IP адреса)",
     "Add comment":
         "Добавить комментарий",
@@ -105,15 +105,15 @@
     "Comment posted.":
         "Комментарий опубликован.",
     "Could not refresh display: %s":
-        "Невозможно обновить данные: %s",
+        "Could not refresh display: %s",
     "unknown status":
         "неизвестная причина",
     "server error or not responding":
         "ошибка сервера или нет ответа",
     "Could not post comment: %s":
         "Не удалось опубликовать комментарий: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Отправка записи (Пожалуйста двигайте мышкой для большей энтропии)…",
+    "Please move your mouse for more entropy…":
+        "Пожалуйста двигайте мышкой для большей энтропии…",
     "Sending paste…":
         "Отправка записи…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -138,7 +138,9 @@
     "Source Code": "Исходный код",
     "Markdown": "Язык разметки",
     "Download attachment": "Скачать прикрепленный файл",
-    "Cloned file attached.": "Дубль файла прикреплен.",
+    "Cloned: '%s'": "Дублировано: '%s'",
+    "The cloned file '%s' was attached to this paste.":
+        "Дубликат файла '%s' был прикреплен к этой записи.",
     "Attach a file": "Прикрепить файл",
     "Remove attachment": "Удалить вложение",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -154,6 +156,10 @@
         "Расшифровать",
     "Enter password":
         "Введите пароль",
-    "Uploading paste… Please wait.":
-        "Отправка записи… Пожалуйста подождите."
+    "Loading…": "Загрузка…",
+    "Decrypting paste…": "Расшифровка записи…",
+    "Preparing new paste…": "Подготовка новой записи…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Если данное сообщение не исчезает длительное время, посмотрите <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">этот FAQ с информацией о возможном решении проблемы (на английском)</a>.",
+    "+++ no paste text +++": "+++ в записи нет текста +++"
 }

+ 11 - 7
i18n/sl.json

@@ -7,8 +7,8 @@
     "en": "sl",
     "Paste does not exist, has expired or has been deleted.":
         "Prilepek ne obstaja, mu je potekla življenjska doba, ali pa je izbrisan.",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "Oprosti, %s za delovanje potrebuje vsaj php 5.3.0.",
+    "%s requires php %s or above to work. Sorry.":
+        "Oprosti, %s za delovanje potrebuje vsaj php %s.",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "Please wait %d seconds between each post.":
@@ -92,7 +92,7 @@
         "Odgovori",
     "Anonymous":
         "Aninomno",
-    "Anonymous avatar (Vizhash of the IP address)":
+    "Avatar generated from IP address":
         "Anonimen avatar (Vizhash IP naslova)",
     "Add comment":
         "Dodaj komentar",
@@ -112,8 +112,8 @@
         "napaka na strežniku, ali pa se strežnik ne odziva",
     "Could not post comment: %s":
         "Komentarja ni bilo mogoče objaviti : %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "Pošiljam prilepek (prosim premakni svojo miško za več entropije) …",
+    "Please move your mouse for more entropy…":
+        "Prosim premakni svojo miško za več entropije…",
     "Sending paste…":
         "Pošiljam prilepek…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -138,7 +138,8 @@
     "Source Code": "Odprta koda",
     "Markdown": "Markdown",
     "Download attachment": "Pretoči priponko",
-    "Cloned file attached.": "Pripeta datoteka klonirana",
+    "Cloned: '%s'": "'%s' klonirana",
+    "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.",
     "Attach a file": "Pripni datoteko",
     "Remove attachment": "Odstrani priponko",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -155,6 +156,9 @@
     "Enter password":
         "Prosim vnesi geslo",
     "Loading…": "Loading…",
+    "Decrypting paste…": "Decrypting paste…",
+    "Preparing new paste…": "Preparing new paste…",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English)."
+        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English).",
+    "+++ no paste text +++": "+++ no paste text +++"
 }

+ 12 - 8
i18n/zh.json

@@ -7,8 +7,8 @@
     "en": "zh",
     "Paste does not exist, has expired or has been deleted.":
         "粘贴不存在,已过期或者已被删除。",
-    "%s requires php 5.3.0 or above to work. Sorry.":
-        "%s需要工作于PHP 5.3.0及以上版本,抱歉。",
+    "%s requires php %s or above to work. Sorry.":
+        "%s需要工作于PHP %s及以上版本,抱歉。",
     "%s requires configuration section [%s] to be present in configuration file.":
         "%s需要设置配置文件中 [%s] 的部分。",
     "Please wait %d seconds between each post.":
@@ -92,8 +92,8 @@
         "回复",
     "Anonymous":
         "匿名",
-    "Anonymous avatar (Vizhash of the IP address)":
-        "匿名头像 (由IP地址生成Vizhash)",
+    "Avatar generated from IP address":
+        "由IP生成的头像",
     "Add comment":
         "添加评论",
     "Optional nickname…":
@@ -112,8 +112,8 @@
         "服务器错误或无回应",
     "Could not post comment: %s":
         "无法发送评论: %s",
-    "Sending paste (Please move your mouse for more entropy)…":
-        "粘贴提交中 (请移动鼠标以产生更多熵)…",
+    "Please move your mouse for more entropy…":
+        "请移动鼠标增加随机性…",
     "Sending paste…":
         "粘贴提交中…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
@@ -129,7 +129,8 @@
     "Source Code": "源代码",
     "Markdown": "Markdown",
     "Download attachment": "下载附件",
-    "Cloned file attached.": "已附加克隆的文件",
+    "Cloned: '%s'": "克隆: '%s'",
+    "The cloned file '%s' was attached to this paste.": "克隆文件 '%s' 已附加到此粘贴。",
     "Attach a file": "添加一个附件",
     "Remove attachment": "移除附件",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -146,6 +147,9 @@
     "Enter password":
         "输入密码",
     "Loading…": "载入中…",
+    "Decrypting paste…": "正在解密",
+    "Preparing new paste…": "正在准备新的粘贴",
     "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
-        "如果这个消息一直不消失,请参考 <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">这里的 FAQ 进行故障排除</a> (英文版)。"
+        "如果这个消息一直不消失,请参考 <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">这里的 FAQ 进行故障排除</a> (英文版)。",
+    "+++ no paste text +++": "+++ 没有粘贴内容 +++"
 }

+ 43 - 59
js/privatebin.js

@@ -179,20 +179,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * @name   Helper.urls2links
          * @function
-         * @param  {Object} element - a jQuery DOM element
+         * @param  {Object} $element - a jQuery DOM element
          */
         me.urls2links = function($element)
         {
             var markup = '<a href="$1" rel="nofollow">$1</a>';
             $element.html(
                 $element.html().replace(
-                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
+                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
                     markup
                 )
             );
             $element.html(
                 $element.html().replace(
-                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
+                    /((magnet):[\w?=&.\/-;#@~%+*-]+)/ig,
                     markup
                 )
             );
@@ -201,6 +201,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * minimal sprintf emulation for %s and %d formats
          *
+         * Note that this function needs the parameters in the same order as the
+         * format strings appear in the string, contrary to the original.
+         *
          * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
          * @name   Helper.sprintf
          * @function
@@ -213,27 +216,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             var args = Array.prototype.slice.call(arguments);
             var format = args[0],
                 i = 1;
-            return format.replace(/%((%)|s|d)/g, function (m) {
+            return format.replace(/%(s|d)/g, function (m) {
                 // m is the matched format, e.g. %s, %d
-                var val;
-                if (m[2]) {
-                    val = m[2];
-                } else {
-                    val = args[i];
-                    // A switch statement so that the formatter can be extended.
-                    switch (m)
-                    {
-                        case '%d':
-                            val = parseFloat(val);
-                            if (isNaN(val)) {
-                                val = 0;
-                            }
-                            break;
-                        default:
-                            // Default is %s
-                    }
-                    ++i;
+                var val = args[i];
+                // A switch statement so that the formatter can be extended.
+                switch (m)
+                {
+                    case '%d':
+                        val = parseFloat(val);
+                        if (isNaN(val)) {
+                            val = 0;
+                        }
+                        break;
+                    default:
+                        // Default is %s
                 }
+                ++i;
                 return val;
             });
         }
@@ -244,7 +242,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
          * @name   Helper.getCookie
          * @function
-         * @param  {string} cname
+         * @param  {string} cname - may not be empty
          * @return {string}
          */
         me.getCookie = function(cname) {
@@ -675,7 +673,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @function
          * @return {string} func
          */
-        me.getSymmetricKey = function(func)
+        me.getSymmetricKey = function()
         {
             return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
         }
@@ -903,8 +901,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         me.isVisible = function($element)
         {
             var elementTop = $element.offset().top;
-            var elementBottom = elementTop + $element.outerHeight();
-
             var viewportTop = $(window).scrollTop();
             var viewportBottom = viewportTop + $(window).height();
 
@@ -985,11 +981,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
      * Alert/error manager
      *
      * @name   Alert
-     * @param  {object} window
-     * @param  {object} document
      * @class
      */
-    var Alert = (function (window, document) {
+    var Alert = (function () {
         var me = {};
 
         var $errorMessage,
@@ -1249,17 +1243,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         return me;
-    })(window, document);
+    })();
 
     /**
      * handles paste status/result
      *
      * @name   PasteStatus
      * @param  {object} window
-     * @param  {object} document
      * @class
      */
-    var PasteStatus = (function (window, document) {
+    var PasteStatus = (function (window) {
         var me = {};
 
         var $pasteSuccess,
@@ -1402,17 +1395,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         return me;
-    })(window, document);
+    })(window);
 
     /**
      * password prompt
      *
      * @name Prompt
-     * @param  {object} window
-     * @param  {object} document
      * @class
      */
-    var Prompt = (function (window, document) {
+    var Prompt = (function () {
         var me = {};
 
         var $passwordDecrypt,
@@ -1512,7 +1503,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         return me;
-    })(window, document);
+    })();
 
     /**
      * Manage paste/message input, and preview tab
@@ -1520,11 +1511,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
      * Note that the actual preview is handled by PasteViewer.
      *
      * @name   Editor
-     * @param  {object} window
-     * @param  {object} document
      * @class
      */
-    var Editor = (function (window, document) {
+    var Editor = (function () {
         var me = {};
 
         var $editorTabs,
@@ -1728,17 +1717,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         return me;
-    })(window, document);
+    })();
 
     /**
      * (view) Parse and show paste.
      *
      * @name   PasteViewer
-     * @param  {object} window
-     * @param  {object} document
      * @class
      */
-    var PasteViewer = (function (window, document) {
+    var PasteViewer = (function () {
         var me = {};
 
         var $placeholder,
@@ -1904,7 +1891,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @function
          * @return {string}
          */
-        me.getText = function(newText)
+        me.getText = function()
         {
             return text;
         }
@@ -1981,7 +1968,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         return me;
-    })(window, document);
+    })();
 
     /**
      * (view) Show attachment and preview if possible
@@ -1998,8 +1985,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             $attachmentPreview,
             $attachment;
 
-        var attachmentChanged = false,
-            attachmentHasPreview = false;
+        var attachmentHasPreview = false;
 
         /**
          * sets the attachment but does not yet show it
@@ -2027,8 +2013,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                 );
                 attachmentHasPreview = true;
             }
-
-            attachmentChanged = true;
         }
 
         /**
@@ -3043,7 +3027,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @private
          * @function
          * @param {int} status
-         * @param {int} data - optional
+         * @param {int} result - optional
          */
         function success(status, result)
         {
@@ -3063,7 +3047,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @private
          * @function
          * @param {int} status - internal code
-         * @param {int} data - original error code
+         * @param {int} result - original error code
          */
         function fail(status, result)
         {
@@ -3107,7 +3091,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * @name   Uploader.setUrl
          * @function
-         * @param {function} func
+         * @param {function} newUrl
          */
         me.setUrl = function(newUrl)
         {
@@ -3236,17 +3220,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @return {array}
          */
         me.parseUploadError = function(status, data, doThisThing) {
-            var errorArray = ['Error while parsing error message.'];
+            var errorArray;
 
             switch (status) {
-                case Uploader.error['custom']:
+                case me.error['custom']:
                     errorArray = ['Could not ' + doThisThing + ': %s', data.message];
                     break;
-                case Uploader.error['unknown']:
+                case me.error['unknown']:
                     errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
                     break;
-                case Uploader.error['serverError']:
-                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];                        break;
+                case me.error['serverError']:
+                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
+                    break;
                 default:
                     errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
                     break;
@@ -3884,7 +3869,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
 
             // show proper elements on screen
             PasteDecrypter.run();
-            return;
         }
 
         /**

+ 219 - 0
js/test.js

@@ -68,6 +68,225 @@ describe('Helper', function () {
         });
     });
 
+    // this test is not yet meaningful using jsdom, as it does not contain getSelection support.
+    // TODO: This needs to be tested using a browser.
+    describe('selectText', function () {
+        jsc.property(
+            'selection contains content of given ID',
+            jsc.nearray(jsc.nearray(jsc.elements(alnumString))),
+            'nearray string',
+            function (ids, contents) {
+                var html = '',
+                    result = true;
+                ids.forEach(function(item, i) {
+                    html += '<div id="' + item.join('') + '">' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
+                });
+                var clean = jsdom(html);
+                ids.forEach(function(item, i) {
+                    $.PrivateBin.Helper.selectText(item.join(''));
+                    // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet.
+                    // Once there is one, uncomment the line below to actually check the result.
+                    //result *= (contents[i] || contents[0]) === window.getSelection().toString();
+                });
+                clean();
+                return Boolean(result);
+            }
+        );
+    });
+
+    describe('setElementText', function () {
+        after(function () {
+            cleanup();
+        });
+
+        jsc.property(
+            'replaces the content of an element',
+            jsc.nearray(jsc.nearray(jsc.elements(alnumString))),
+            'nearray string',
+            'string',
+            function (ids, contents, replacingContent) {
+                var html = '',
+                    result = true;
+                ids.forEach(function(item, i) {
+                    html += '<div id="' + item.join('') + '">' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
+                });
+                var elements = $('<body />').html(html);
+                ids.forEach(function(item, i) {
+                    var id = item.join(''),
+                        element = elements.find('#' + id).first();
+                    $.PrivateBin.Helper.setElementText(element, replacingContent);
+                    result *= replacingContent === element.text();
+                });
+                return Boolean(result);
+            }
+        );
+    });
+
+    describe('urls2links', function () {
+        after(function () {
+            cleanup();
+        });
+
+        jsc.property(
+            'ignores non-URL content',
+            'string',
+            function (content) {
+                var element = $('<div>' + content + '</div>'),
+                    before = element.html();
+                $.PrivateBin.Helper.urls2links(element);
+                return before === element.html();
+            }
+        );
+        jsc.property(
+            'replaces URLs with anchors',
+            'string',
+            jsc.elements(['http', 'https', 'ftp']),
+            jsc.nearray(jsc.elements(a2zString)),
+            jsc.array(jsc.elements(queryString)),
+            jsc.array(jsc.elements(queryString)),
+            'string',
+            function (prefix, schema, address, query, fragment, postfix) {
+                var query = query.join(''),
+                    fragment = fragment.join(''),
+                    url = schema + '://' + address.join('') + '/?' + query + '#' + fragment,
+                    prefix = $.PrivateBin.Helper.htmlEntities(prefix),
+                    postfix = ' ' + $.PrivateBin.Helper.htmlEntities(postfix),
+                    element = $('<div>' + prefix + url + postfix + '</div>');
+
+                // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 or &#x
+                if (
+                    query.slice(-1) === '&' &&
+                    (parseInt(fragment.substring(0, 1), 10) >= 0 || fragment.charAt(0) === 'x' )
+                )
+                {
+                    url = schema + '://' + address.join('') + '/?' + query.substring(0, query.length - 1);
+                    postfix = '';
+                    element = $('<div>' + prefix + url + '</div>');
+                }
+
+                $.PrivateBin.Helper.urls2links(element);
+                return element.html() === $('<div>' + prefix + '<a href="' + url + '" rel="nofollow">' + url + '</a>' + postfix + '</div>').html();
+            }
+        );
+        jsc.property(
+            'replaces magnet links with anchors',
+            'string',
+            jsc.array(jsc.elements(queryString)),
+            'string',
+            function (prefix, query, postfix) {
+                var url = 'magnet:?' + query.join(''),
+                    prefix = $.PrivateBin.Helper.htmlEntities(prefix),
+                    postfix = $.PrivateBin.Helper.htmlEntities(postfix),
+                    element = $('<div>' + prefix + url + ' ' + postfix + '</div>');
+                $.PrivateBin.Helper.urls2links(element);
+                return element.html() === $('<div>' + prefix + '<a href="' + url + '" rel="nofollow">' + url + '</a> ' + postfix + '</div>').html();
+            }
+        );
+    });
+
+    describe('sprintf', function () {
+        after(function () {
+            cleanup();
+        });
+
+        jsc.property(
+            'replaces %s in strings with first given parameter',
+            'string',
+            '(small nearray) string',
+            'string',
+            function (prefix, params, postfix) {
+                var prefix = prefix.replace(/%(s|d)/g, '%%'),
+                    postfix = postfix.replace(/%(s|d)/g, '%%'),
+                    result = prefix + params[0] + postfix;
+                params.unshift(prefix + '%s' + postfix);
+                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
+            }
+        );
+        jsc.property(
+            'replaces %d in strings with first given parameter',
+            'string',
+            '(small nearray) nat',
+            'string',
+            function (prefix, params, postfix) {
+                var prefix = prefix.replace(/%(s|d)/g, '%%'),
+                    postfix = postfix.replace(/%(s|d)/g, '%%'),
+                    result = prefix + params[0] + postfix;
+                params.unshift(prefix + '%d' + postfix);
+                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
+            }
+        );
+        jsc.property(
+            'replaces %d in strings with 0 if first parameter is not a number',
+            'string',
+            '(small nearray) falsy',
+            'string',
+            function (prefix, params, postfix) {
+                var prefix = prefix.replace(/%(s|d)/g, '%%'),
+                    postfix = postfix.replace(/%(s|d)/g, '%%'),
+                    result = prefix + '0' + postfix;
+                params.unshift(prefix + '%d' + postfix);
+                return result === $.PrivateBin.Helper.sprintf.apply(this, params)
+            }
+        );
+        jsc.property(
+            'replaces %d and %s in strings in order',
+            'string',
+            'nat',
+            'string',
+            'string',
+            'string',
+            function (prefix, uint, middle, string, postfix) {
+                var prefix = prefix.replace(/%(s|d)/g, '%%'),
+                    postfix = postfix.replace(/%(s|d)/g, '%%'),
+                    params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
+                    result = prefix + uint + middle + string + postfix;
+                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
+            }
+        );
+        jsc.property(
+            'replaces %d and %s in strings in reverse order',
+            'string',
+            'nat',
+            'string',
+            'string',
+            'string',
+            function (prefix, uint, middle, string, postfix) {
+                var prefix = prefix.replace(/%(s|d)/g, '%%'),
+                    postfix = postfix.replace(/%(s|d)/g, '%%'),
+                    params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
+                    result = prefix + string + middle + uint + postfix;
+                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
+            }
+        );
+    });
+
+    describe('getCookie', function () {
+        jsc.property(
+            'returns the requested cookie',
+            'nearray asciinestring',
+            'nearray asciistring',
+            function (labels, values) {
+                var selectedKey = '', selectedValue = '',
+                    cookieArray = [],
+                    count = 0;
+                labels.forEach(function(item, i) {
+                    var key = item.replace(/[\s;,=]/g, 'x'),
+                        value = (values[i] || values[0]).replace(/[\s;,=]/g, '');
+                    cookieArray.push(key + '=' + value);
+                    if (Math.random() < 1 / i)
+                    {
+                        selectedKey = key;
+                        selectedValue = value;
+                    }
+                });
+                var clean = jsdom('', {cookie: cookieArray}),
+                    result = $.PrivateBin.Helper.getCookie(selectedKey);
+                clean();
+                return result === selectedValue;
+            }
+        );
+    });
+
     describe('baseUri', function () {
         before(function () {
             $.PrivateBin.Helper.reset();

+ 1 - 3
lib/Data/AbstractData.php

@@ -58,7 +58,7 @@ abstract class AbstractData
      * @access public
      * @static
      * @param  array $options
-     * @return privatebin_abstract
+     * @return AbstractData
      */
     public static function getInstance($options)
     {
@@ -88,7 +88,6 @@ abstract class AbstractData
      *
      * @access public
      * @param  string $pasteid
-     * @return void
      */
     abstract public function delete($pasteid);
 
@@ -147,7 +146,6 @@ abstract class AbstractData
      *
      * @access public
      * @param  int $batchsize
-     * @return void
      */
     public function purge($batchsize)
     {

+ 4 - 10
lib/Data/Database.php

@@ -282,7 +282,6 @@ class Database extends AbstractData
      *
      * @access public
      * @param  string $pasteid
-     * @return void
      */
     public function delete($pasteid)
     {
@@ -375,11 +374,10 @@ class Database extends AbstractData
                 $comments[$i]->data           = $row['data'];
                 $comments[$i]->meta           = new stdClass;
                 $comments[$i]->meta->postdate = (int) $row['postdate'];
-                if (array_key_exists('nickname', $row) && !empty($row['nickname'])) {
-                    $comments[$i]->meta->nickname = $row['nickname'];
-                }
-                if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) {
-                    $comments[$i]->meta->vizhash = $row['vizhash'];
+                foreach (array('nickname', 'vizhash') as $key) {
+                    if (array_key_exists($key, $row) && !empty($row[$key])) {
+                        $comments[$i]->meta->$key = $row[$key];
+                    }
                 }
             }
             ksort($comments);
@@ -564,7 +562,6 @@ class Database extends AbstractData
      *
      * @access private
      * @static
-     * @return void
      */
     private static function _createPasteTable()
     {
@@ -589,7 +586,6 @@ class Database extends AbstractData
      *
      * @access private
      * @static
-     * @return void
      */
     private static function _createCommentTable()
     {
@@ -616,7 +612,6 @@ class Database extends AbstractData
      *
      * @access private
      * @static
-     * @return void
      */
     private static function _createConfigTable()
     {
@@ -651,7 +646,6 @@ class Database extends AbstractData
      * @access private
      * @static
      * @param  string $oldversion
-     * @return void
      */
     private static function _upgradeDatabase($oldversion)
     {

+ 23 - 54
lib/Data/Filesystem.php

@@ -12,8 +12,8 @@
 
 namespace PrivateBin\Data;
 
-use PrivateBin\Json;
 use PrivateBin\Model\Paste;
+use PrivateBin\Persistence\DataStore;
 
 /**
  * Filesystem
@@ -22,15 +22,6 @@ use PrivateBin\Model\Paste;
  */
 class Filesystem extends AbstractData
 {
-    /**
-     * directory where data is stored
-     *
-     * @access private
-     * @static
-     * @var string
-     */
-    private static $_dir = 'data/';
-
     /**
      * get instance of singleton
      *
@@ -41,17 +32,16 @@ class Filesystem extends AbstractData
      */
     public static function getInstance($options = null)
     {
+        // if needed initialize the singleton
+        if (!(self::$_instance instanceof self)) {
+            self::$_instance = new self;
+        }
         // if given update the data directory
         if (
             is_array($options) &&
             array_key_exists('dir', $options)
         ) {
-            self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
-        }
-        // if needed initialize the singleton
-        if (!(self::$_instance instanceof self)) {
-            self::$_instance = new self;
-            self::_init();
+            DataStore::setPath($options['dir']);
         }
         return self::$_instance;
     }
@@ -62,19 +52,19 @@ class Filesystem extends AbstractData
      * @access public
      * @param  string $pasteid
      * @param  array  $paste
-     * @throws Exception
      * @return bool
      */
     public function create($pasteid, $paste)
     {
         $storagedir = self::_dataid2path($pasteid);
-        if (is_file($storagedir . $pasteid)) {
+        $file       = $storagedir . $pasteid;
+        if (is_file($file)) {
             return false;
         }
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return (bool) file_put_contents($storagedir . $pasteid, Json::encode($paste));
+        return DataStore::store($file, $paste);
     }
 
     /**
@@ -108,7 +98,6 @@ class Filesystem extends AbstractData
      *
      * @access public
      * @param  string $pasteid
-     * @return void
      */
     public function delete($pasteid)
     {
@@ -155,20 +144,19 @@ class Filesystem extends AbstractData
      * @param  string $parentid
      * @param  string $commentid
      * @param  array  $comment
-     * @throws Exception
      * @return bool
      */
     public function createComment($pasteid, $parentid, $commentid, $comment)
     {
         $storagedir = self::_dataid2discussionpath($pasteid);
-        $filename   = $pasteid . '.' . $commentid . '.' . $parentid;
-        if (is_file($storagedir . $filename)) {
+        $file       = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid;
+        if (is_file($file)) {
             return false;
         }
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return (bool) file_put_contents($storagedir . $filename, Json::encode($comment));
+        return DataStore::store($file, $comment);
     }
 
     /**
@@ -237,8 +225,9 @@ class Filesystem extends AbstractData
     protected function _getExpiredPastes($batchsize)
     {
         $pastes     = array();
+        $mainpath   = DataStore::getPath();
         $firstLevel = array_filter(
-            scandir(self::$_dir),
+            scandir($mainpath),
             'self::_isFirstLevelDir'
         );
         if (count($firstLevel) > 0) {
@@ -246,7 +235,7 @@ class Filesystem extends AbstractData
             for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
                 $firstKey    = array_rand($firstLevel);
                 $secondLevel = array_filter(
-                    scandir(self::$_dir . $firstLevel[$firstKey]),
+                    scandir($mainpath . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
                     'self::_isSecondLevelDir'
                 );
 
@@ -257,8 +246,9 @@ class Filesystem extends AbstractData
                 }
 
                 $secondKey = array_rand($secondLevel);
-                $path      = self::$_dir . $firstLevel[$firstKey] .
-                    DIRECTORY_SEPARATOR . $secondLevel[$secondKey];
+                $path      = $mainpath . DIRECTORY_SEPARATOR .
+                    $firstLevel[$firstKey] . DIRECTORY_SEPARATOR .
+                    $secondLevel[$secondKey];
                 if (!is_dir($path)) {
                     continue;
                 }
@@ -292,29 +282,6 @@ class Filesystem extends AbstractData
         return $pastes;
     }
 
-    /**
-     * initialize privatebin
-     *
-     * @access private
-     * @static
-     * @return void
-     */
-    private static function _init()
-    {
-        // Create storage directory if it does not exist.
-        if (!is_dir(self::$_dir)) {
-            mkdir(self::$_dir, 0700);
-        }
-        // Create .htaccess file if it does not exist.
-        if (!is_file(self::$_dir . '.htaccess')) {
-            file_put_contents(
-                self::$_dir . '.htaccess',
-                'Allow from none' . PHP_EOL .
-                'Deny from all' . PHP_EOL
-            );
-        }
-    }
-
     /**
      * Convert paste id to storage path.
      *
@@ -332,8 +299,10 @@ class Filesystem extends AbstractData
      */
     private static function _dataid2path($dataid)
     {
-        return self::$_dir . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
-            substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
+        return DataStore::getPath(
+            substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
+            substr($dataid, 2, 2) . DIRECTORY_SEPARATOR
+        );
     }
 
     /**
@@ -363,7 +332,7 @@ class Filesystem extends AbstractData
     private static function _isFirstLevelDir($element)
     {
         return self::_isSecondLevelDir($element) &&
-            is_dir(self::$_dir . DIRECTORY_SEPARATOR . $element);
+            is_dir(DataStore::getPath($element));
     }
 
     /**

+ 5 - 4
lib/I18n.php

@@ -135,15 +135,17 @@ class I18n
      *
      * @access public
      * @static
-     * @return void
      */
     public static function loadTranslations()
     {
         $availableLanguages = self::getAvailableLanguages();
 
         // check if the lang cookie was set and that language exists
-        if (array_key_exists('lang', $_COOKIE) && in_array($_COOKIE['lang'], $availableLanguages)) {
-            $match = $_COOKIE['lang'];
+        if (
+            array_key_exists('lang', $_COOKIE) &&
+            ($key = array_search($_COOKIE['lang'], $availableLanguages)) !== false
+        ) {
+            $match = $availableLanguages[$key];
         }
         // find a translation file matching the browsers language preferences
         else {
@@ -256,7 +258,6 @@ class I18n
      * @access public
      * @static
      * @param  string $lang
-     * @return void
      */
     public static function setLanguageFallback($lang)
     {

+ 0 - 3
lib/Model.php

@@ -40,7 +40,6 @@ class Model
      * Factory constructor.
      *
      * @param configuration $conf
-     * @return void
      */
     public function __construct(Configuration $conf)
     {
@@ -64,8 +63,6 @@ class Model
 
     /**
      * Checks if a purge is necessary and triggers it if yes.
-     *
-     * @return void
      */
     public function purge()
     {

+ 0 - 5
lib/Model/AbstractModel.php

@@ -63,7 +63,6 @@ abstract class AbstractModel
      * @access public
      * @param  Configuration $configuration
      * @param  AbstractData $storage
-     * @return void
      */
     public function __construct(Configuration $configuration, AbstractData $storage)
     {
@@ -90,7 +89,6 @@ abstract class AbstractModel
      * @access public
      * @param string $id
      * @throws Exception
-     * @return void
      */
     public function setId($id)
     {
@@ -106,7 +104,6 @@ abstract class AbstractModel
      * @access public
      * @param string $data
      * @throws Exception
-     * @return void
      */
     public function setData($data)
     {
@@ -133,7 +130,6 @@ abstract class AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     abstract public function store();
 
@@ -142,7 +138,6 @@ abstract class AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     abstract public function delete();
 

+ 0 - 5
lib/Model/Comment.php

@@ -61,7 +61,6 @@ class Comment extends AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     public function store()
     {
@@ -101,7 +100,6 @@ class Comment extends AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     public function delete()
     {
@@ -129,7 +127,6 @@ class Comment extends AbstractModel
      * @access public
      * @param Paste $paste
      * @throws Exception
-     * @return void
      */
     public function setPaste(Paste $paste)
     {
@@ -154,7 +151,6 @@ class Comment extends AbstractModel
      * @access public
      * @param string $id
      * @throws Exception
-     * @return void
      */
     public function setParentId($id)
     {
@@ -184,7 +180,6 @@ class Comment extends AbstractModel
      * @access public
      * @param string $nickname
      * @throws Exception
-     * @return void
      */
     public function setNickname($nickname)
     {

+ 0 - 8
lib/Model/Paste.php

@@ -75,7 +75,6 @@ class Paste extends AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     public function store()
     {
@@ -103,7 +102,6 @@ class Paste extends AbstractModel
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     public function delete()
     {
@@ -183,7 +181,6 @@ class Paste extends AbstractModel
      * @access public
      * @param string $attachment
      * @throws Exception
-     * @return void
      */
     public function setAttachment($attachment)
     {
@@ -199,7 +196,6 @@ class Paste extends AbstractModel
      * @access public
      * @param string $attachmentname
      * @throws Exception
-     * @return void
      */
     public function setAttachmentName($attachmentname)
     {
@@ -214,7 +210,6 @@ class Paste extends AbstractModel
      *
      * @access public
      * @param string $expiration
-     * @return void
      */
     public function setExpiration($expiration)
     {
@@ -236,7 +231,6 @@ class Paste extends AbstractModel
      * @access public
      * @param string $burnafterreading
      * @throws Exception
-     * @return void
      */
     public function setBurnafterreading($burnafterreading = '1')
     {
@@ -257,7 +251,6 @@ class Paste extends AbstractModel
      * @access public
      * @param string $opendiscussion
      * @throws Exception
-     * @return void
      */
     public function setOpendiscussion($opendiscussion = '1')
     {
@@ -281,7 +274,6 @@ class Paste extends AbstractModel
      * @access public
      * @param string $format
      * @throws Exception
-     * @return void
      */
     public function setFormatter($format)
     {

+ 3 - 8
lib/Persistence/AbstractPersistence.php

@@ -36,7 +36,6 @@ abstract class AbstractPersistence
      * @access public
      * @static
      * @param  string $path
-     * @return void
      */
     public static function setPath($path)
     {
@@ -80,27 +79,23 @@ abstract class AbstractPersistence
      * @access protected
      * @static
      * @throws Exception
-     * @return void
      */
     protected static function _initialize()
     {
         // Create storage directory if it does not exist.
         if (!is_dir(self::$_path)) {
-            if (!@mkdir(self::$_path)) {
+            if (!@mkdir(self::$_path, 0700)) {
                 throw new Exception('unable to create directory ' . self::$_path, 10);
             }
         }
-
-        // Create .htaccess file if it does not exist.
         $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
         if (!is_file($file)) {
             $writtenBytes = @file_put_contents(
                 $file,
-                'Allow from none' . PHP_EOL .
-                'Deny from all' . PHP_EOL,
+                'Require all denied' . PHP_EOL,
                 LOCK_EX
             );
-            if ($writtenBytes === false || $writtenBytes < 30) {
+            if ($writtenBytes === false || $writtenBytes < 19) {
                 throw new Exception('unable to write to file ' . $file, 11);
             }
         }

+ 47 - 0
lib/Persistence/DataStore.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * PrivateBin
+ *
+ * a zero-knowledge paste bin
+ *
+ * @link      https://github.com/PrivateBin/PrivateBin
+ * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
+ * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
+ * @version   1.1
+ */
+
+namespace PrivateBin\Persistence;
+
+use Exception;
+use PrivateBin\Json;
+
+/**
+ * DataStore
+ *
+ * Handles data storage for Data\Filesystem.
+ */
+class DataStore extends AbstractPersistence
+{
+    /**
+     * store the data
+     *
+     * @access public
+     * @static
+     * @param  string $filename
+     * @param  array  $data
+     * @return bool
+     */
+    public static function store($filename, $data)
+    {
+        $path = self::getPath();
+        if (strpos($filename, $path) === 0) {
+            $filename = substr($filename, strlen($path));
+        }
+        try {
+            self::_store($filename, Json::encode($data));
+            return true;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+}

+ 0 - 2
lib/Persistence/PurgeLimiter.php

@@ -36,7 +36,6 @@ class PurgeLimiter extends AbstractPersistence
      * @access public
      * @static
      * @param  int $limit
-     * @return void
      */
     public static function setLimit($limit)
     {
@@ -49,7 +48,6 @@ class PurgeLimiter extends AbstractPersistence
      * @access public
      * @static
      * @param Configuration $conf
-     * @return void
      */
     public static function setConfiguration(Configuration $conf)
     {

+ 0 - 1
lib/Persistence/ServerSalt.php

@@ -95,7 +95,6 @@ class ServerSalt extends AbstractPersistence
      * @access public
      * @static
      * @param  string $path
-     * @return void
      */
     public static function setPath($path)
     {

+ 0 - 2
lib/Persistence/TrafficLimiter.php

@@ -45,7 +45,6 @@ class TrafficLimiter extends AbstractPersistence
      * @access public
      * @static
      * @param  int $limit
-     * @return void
      */
     public static function setLimit($limit)
     {
@@ -58,7 +57,6 @@ class TrafficLimiter extends AbstractPersistence
      * @access public
      * @static
      * @param Configuration $conf
-     * @return void
      */
     public static function setConfiguration(Configuration $conf)
     {

+ 9 - 20
lib/PrivateBin.php

@@ -30,6 +30,13 @@ class PrivateBin
      */
     const VERSION = '1.1';
 
+    /**
+     * minimal required PHP version
+     *
+     * @const string
+     */
+    const MIN_PHP_VERSION = '5.4.0';
+
     /**
      * show the same error message if the paste expired or does not exist
      *
@@ -116,12 +123,11 @@ class PrivateBin
      *
      * @access public
      * @throws Exception
-     * @return void
      */
     public function __construct()
     {
-        if (version_compare(PHP_VERSION, '5.4.0') < 0) {
-            throw new Exception(I18n::_('%s requires php 5.4.0 or above to work. Sorry.', I18n::_('PrivateBin')), 1);
+        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
+            throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1);
         }
         if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
             throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5);
@@ -164,21 +170,9 @@ class PrivateBin
      * initialize privatebin
      *
      * @access private
-     * @return void
      */
     private function _init()
     {
-        foreach (array('cfg', 'lib') as $dir) {
-            if (!is_file(PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess')) {
-                file_put_contents(
-                PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess',
-                'Allow from none' . PHP_EOL .
-                'Deny from all' . PHP_EOL,
-                LOCK_EX
-            );
-            }
-        }
-
         $this->_conf    = new Configuration;
         $this->_model   = new Model($this->_conf);
         $this->_request = new Request;
@@ -324,7 +318,6 @@ class PrivateBin
      * @access private
      * @param  string $dataid
      * @param  string $deletetoken
-     * @return void
      */
     private function _delete($dataid, $deletetoken)
     {
@@ -368,7 +361,6 @@ class PrivateBin
      *
      * @access private
      * @param  string $dataid
-     * @return void
      */
     private function _read($dataid)
     {
@@ -401,7 +393,6 @@ class PrivateBin
      * Display PrivateBin frontend.
      *
      * @access private
-     * @return void
      */
     private function _view()
     {
@@ -465,7 +456,6 @@ class PrivateBin
      *
      * @access private
      * @param string $type
-     * @return void
      */
     private function _jsonld($type)
     {
@@ -498,7 +488,6 @@ class PrivateBin
      * @param  int $status
      * @param  string $message
      * @param  array $other
-     * @return void
      */
     private function _return_message($status, $message, $other = array())
     {

+ 10 - 11
lib/Request.php

@@ -41,7 +41,7 @@ class Request
     const MIME_XHTML = 'application/xhtml+xml';
 
     /**
-     * Input stream to use for PUT parameter parsing.
+     * Input stream to use for PUT parameter parsing
      *
      * @access private
      * @var string
@@ -49,7 +49,7 @@ class Request
     private static $_inputStream = 'php://input';
 
     /**
-     * Operation to perform.
+     * Operation to perform
      *
      * @access private
      * @var string
@@ -57,7 +57,7 @@ class Request
     private $_operation = 'view';
 
     /**
-     * Request parameters.
+     * Request parameters
      *
      * @access private
      * @var array
@@ -65,7 +65,7 @@ class Request
     private $_params = array();
 
     /**
-     * If we are in a JSON API context.
+     * If we are in a JSON API context
      *
      * @access private
      * @var bool
@@ -73,10 +73,9 @@ class Request
     private $_isJsonApi = false;
 
     /**
-     * Constructor.
+     * Constructor
      *
      * @access public
-     * @return void
      */
     public function __construct()
     {
@@ -122,7 +121,7 @@ class Request
     }
 
     /**
-     * Get current operation.
+     * Get current operation
      *
      * @access public
      * @return string
@@ -133,7 +132,7 @@ class Request
     }
 
     /**
-     * Get a request parameter.
+     * Get a request parameter
      *
      * @access public
      * @param  string $param
@@ -146,7 +145,7 @@ class Request
     }
 
     /**
-     * If we are in a JSON API context.
+     * If we are in a JSON API context
      *
      * @access public
      * @return bool
@@ -157,7 +156,7 @@ class Request
     }
 
     /**
-     * Override the default input stream source, used for unit testing.
+     * Override the default input stream source, used for unit testing
      *
      * @param string $input
      */
@@ -167,7 +166,7 @@ class Request
     }
 
     /**
-     * detect the clients supported media type and decide if its a JSON API call or not
+     * Detect the clients supported media type and decide if its a JSON API call or not
      *
      * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
      *

+ 0 - 2
lib/View.php

@@ -35,7 +35,6 @@ class View
      * @access public
      * @param  string $name
      * @param  mixed  $value
-     * @return void
      */
     public function assign($name, $value)
     {
@@ -48,7 +47,6 @@ class View
      * @access public
      * @param  string $template
      * @throws Exception
-     * @return void
      */
     public function draw($template)
     {

+ 0 - 2
lib/Vizhash16x16.php

@@ -61,7 +61,6 @@ class Vizhash16x16
      * constructor
      *
      * @access public
-     * @return void
      */
     public function __construct()
     {
@@ -210,7 +209,6 @@ class Vizhash16x16
      * @param  resource $image
      * @param  int $action
      * @param  int $color
-     * @return void
      */
     private function drawshape($image, $action, $color)
     {

+ 1 - 1
tpl/bootstrap.php

@@ -69,7 +69,7 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7/bLmiTErcH3lMJi1JxXk/lb20NUO7TCqasWeFybnNhnfOUnJ9GQGqPVJxPf+7fnw9dgLa1s18FXQuRanLD8Hw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-xfoNRo43UxDcd7m1P2IsvSPcOeGJo1oUjbA+CNCBoBilKRU3Iuqbf7awv3knP50bX7PVGEjtS6yJ2KtOtqnhTA==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->

+ 1 - 1
tpl/page.php

@@ -47,7 +47,7 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7/bLmiTErcH3lMJi1JxXk/lb20NUO7TCqasWeFybnNhnfOUnJ9GQGqPVJxPf+7fnw9dgLa1s18FXQuRanLD8Hw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-xfoNRo43UxDcd7m1P2IsvSPcOeGJo1oUjbA+CNCBoBilKRU3Iuqbf7awv3knP50bX7PVGEjtS6yJ2KtOtqnhTA==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->

+ 0 - 1
tst/.gitignore

@@ -1 +0,0 @@
-/ConfigurationCombinationsTest.php

+ 0 - 2
tst/.htaccess

@@ -1,2 +0,0 @@
-Allow from none
-Deny from all

+ 15 - 13
tst/Bootstrap.php

@@ -172,22 +172,24 @@ class Helper
      */
     public static function rmDir($path)
     {
-        $path .= DIRECTORY_SEPARATOR;
-        $dir = dir($path);
-        while (false !== ($file = $dir->read())) {
-            if ($file != '.' && $file != '..') {
-                if (is_dir($path . $file)) {
-                    self::rmDir($path . $file);
-                } elseif (is_file($path . $file)) {
-                    if (!unlink($path . $file)) {
-                        throw new Exception('Error deleting file "' . $path . $file . '".');
+        if (is_dir($path)) {
+            $path .= DIRECTORY_SEPARATOR;
+            $dir = dir($path);
+            while (false !== ($file = $dir->read())) {
+                if ($file != '.' && $file != '..') {
+                    if (is_dir($path . $file)) {
+                        self::rmDir($path . $file);
+                    } elseif (is_file($path . $file)) {
+                        if (!unlink($path . $file)) {
+                            throw new Exception('Error deleting file "' . $path . $file . '".');
+                        }
                     }
                 }
             }
-        }
-        $dir->close();
-        if (!rmdir($path)) {
-            throw new Exception('Error deleting directory "' . $path . '".');
+            $dir->close();
+            if (!rmdir($path)) {
+                throw new Exception('Error deleting directory "' . $path . '".');
+            }
         }
     }
 

+ 1 - 1
tst/Data/DatabaseTest.php

@@ -311,7 +311,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
             'vizhash BLOB, ' .
             'postdate INT );'
         );
-        $this->assertInstanceOf(Database::class, Database::getInstance($this->_options));
+        $this->assertInstanceOf('PrivateBin\\Data\\Database', Database::getInstance($this->_options));
 
         // check if version number was upgraded in created configuration table
         $statement = $db->prepare('SELECT value FROM foo_config WHERE id LIKE ?');

+ 13 - 10
tst/Data/FilesystemTest.php

@@ -8,16 +8,26 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
 
     private $_path;
 
+    private $_invalidPath;
+
     public function setUp()
     {
         /* Setup Routine */
-        $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+        $this->_path        = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
+        $this->_model       = Filesystem::getInstance(array('dir' => $this->_path));
+        if (!is_dir($this->_path)) {
+            mkdir($this->_path);
+        }
+        if (!is_dir($this->_invalidPath)) {
+            mkdir($this->_invalidPath);
+        }
     }
 
     public function tearDown()
     {
         /* Tear Down Routine */
+        chmod($this->_invalidPath, 0700);
         Helper::rmDir($this->_path);
     }
 
@@ -37,6 +47,7 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'store comment');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
+        $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'unable to store the same comment twice');
         $comment           = json_decode(json_encode(Helper::getComment()));
         $comment->id       = Helper::getCommentId();
         $comment->parentid = Helper::getPasteId();
@@ -99,10 +110,6 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         }
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 90
-     */
     public function testErrorDetection()
     {
         $this->_model->delete(Helper::getPasteId());
@@ -112,10 +119,6 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist');
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 90
-     */
     public function testCommentErrorDetection()
     {
         $this->_model->delete(Helper::getPasteId());

+ 32 - 0
tst/I18nTest.php

@@ -142,4 +142,36 @@ class I18nTest extends PHPUnit_Framework_TestCase
         I18n::loadTranslations();
         $this->assertEquals('some string + 1', I18n::_('some %s + %d', 'string', 1), 'browser language en');
     }
+
+    public function testMessageIdsExistInAllLanguages()
+    {
+        $messageIds = array();
+        $languages  = array();
+        $dir        = dir(PATH . 'i18n');
+        while (false !== ($file = $dir->read())) {
+            if (strlen($file) === 7) {
+                $language             = substr($file, 0, 2);
+                $languageMessageIds   = array_keys(
+                    json_decode(
+                        file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file),
+                        true
+                    )
+                );
+                $messageIds           = array_unique(array_merge($messageIds, $languageMessageIds));
+                $languages[$language] = $languageMessageIds;
+            }
+        }
+        foreach ($messageIds as $messageId) {
+            foreach (array_keys($languages) as $language) {
+                // most languages don't translate the data size units, ignore those
+                if ($messageId !== 'B' && strlen($messageId) !== 3 && strpos($messageId, 'B', 2) !== 2) {
+                    $this->assertContains(
+                        $messageId,
+                        $languages[$language],
+                        "message ID '$messageId' exists in translation file $language.json"
+                    );
+                }
+            }
+        }
+    }
 }

+ 2 - 0
tst/JsonApiTest.php

@@ -98,6 +98,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
+        unlink($file);
         $response = json_decode($content, true);
         $this->assertEquals(0, $response['status'], 'outputs status');
         $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputted paste ID matches input');
@@ -132,6 +133,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
+        unlink($file);
         $response = json_decode($content, true);
         $this->assertEquals(0, $response['status'], 'outputs status');
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');

+ 53 - 0
tst/ModelTest.php

@@ -82,6 +82,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $comment = $paste->getComment(Helper::getPasteId());
         $comment->setData($commentData['data']);
         $comment->setNickname($commentData['meta']['nickname']);
+        $comment->getParentId();
         $comment->store();
 
         $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId());
@@ -189,6 +190,27 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $this->assertFalse(Paste::isValidId('../bar/baz'), 'path attack');
     }
 
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 64
+     */
+    public function testInvalidPaste()
+    {
+        $this->_model->getPaste(Helper::getPasteId())->delete();
+        $paste = $this->_model->getPaste(Helper::getPasteId());
+        $paste->get();
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 61
+     */
+    public function testInvalidData()
+    {
+        $paste = $this->_model->getPaste();
+        $paste->setData('');
+    }
+
     /**
      * @expectedException Exception
      * @expectedExceptionCode 62
@@ -199,6 +221,37 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $paste->getComment(Helper::getPasteId());
     }
 
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 67
+     */
+    public function testInvalidCommentDeletedPaste()
+    {
+        $pasteData = Helper::getPaste();
+        $paste     = $this->_model->getPaste(Helper::getPasteId());
+        $paste->setData($pasteData['data']);
+        $paste->store();
+
+        $comment = $paste->getComment(Helper::getPasteId());
+        $paste->delete();
+        $comment->store();
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 68
+     */
+    public function testInvalidCommentData()
+    {
+        $pasteData = Helper::getPaste();
+        $paste     = $this->_model->getPaste(Helper::getPasteId());
+        $paste->setData($pasteData['data']);
+        $paste->store();
+
+        $comment = $paste->getComment(Helper::getPasteId());
+        $comment->store();
+    }
+
     public function testExpiration()
     {
         $pasteData = Helper::getPaste();

+ 9 - 12
tst/PrivateBinTest.php

@@ -140,21 +140,18 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testHtaccess()
     {
         $this->reset();
-        $dirs = array('cfg', 'lib');
-        foreach ($dirs as $dir) {
-            $file = PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess';
-            @unlink($file);
-        }
+        $file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
+        @unlink($file);
+
+        $_POST                            = Helper::getPaste();
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         ob_end_clean();
-        foreach ($dirs as $dir) {
-            $file = PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess';
-            $this->assertFileExists(
-                $file,
-                "$dir htaccess recreated"
-            );
-        }
+
+        $this->assertFileExists($file, 'htaccess recreated');
     }
 
     /**

+ 1 - 0
tst/RequestTest.php

@@ -63,6 +63,7 @@ class RequestTest extends PHPUnit_Framework_TestCase
         file_put_contents($file, 'data=foo');
         Request::setInputStream($file);
         $request = new Request;
+        unlink($file);
         $this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
         $this->assertEquals('create', $request->getOperation());
         $this->assertEquals('foo', $request->getParam('data'));

+ 2 - 0
tst/SjclTest.php

@@ -1,11 +1,13 @@
 <?php
 
+use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Sjcl;
 
 class SjclTest extends PHPUnit_Framework_TestCase
 {
     public function testSjclValidatorValidatesCorrectly()
     {
+        ServerSalt::setPath(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data');
         $paste = Helper::getPasteWithAttachment();
         $this->assertTrue(Sjcl::isValid($paste['data']), 'valid sjcl');
         $this->assertTrue(Sjcl::isValid($paste['attachment']), 'valid sjcl');