diff --git a/.codeclimate.yml b/.codeclimate.yml index 01766cd..b530134 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,5 +1,19 @@ --- -engines: +version: "2" +checks: + file-lines: + config: + threshold: 2000 + method-complexity: + config: + threshold: 550 + method-count: + config: + threshold: 50 + method-lines: + config: + threshold: 250 +plugins: csslint: enabled: true duplication: @@ -12,6 +26,8 @@ engines: enabled: true fixme: enabled: true + nodesecurity: + enabled: true phpmd: enabled: true checks: @@ -29,11 +45,20 @@ engines: enabled: false CleanCode/StaticAccess: enabled: false -ratings: - paths: - - "css/privatebin.css" - - "css/bootstrap/privatebin.css" - - "js/privatebin.js" - - "lib/**.php" - - "index.php" -exclude_paths: [] + sonar-php: + enabled: true + config: + tests_patterns: + - tst/** +exclude_patterns: + - "cfg/" + - "css/" + - "!css/privatebin.css" + - "!css/noscript.css" + - "!css/bootstrap/privatebin.css" + - "js/" + - "!js/privatebin.js" + - "!js/common.js" + - "!js/test/" + - "vendor/" + diff --git a/.eslintignore b/.eslintignore index 96212a3..f3c9e2a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -**/*{.,-}min.js +js/*.js +!js/privatebin.js diff --git a/.eslintrc b/.eslintrc index e2a42cc..1f7106f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,15 @@ env: globals: sjcl: false + DOMPurify: false + after: true + before: true + cleanup: true + describe: false + it: false + jsc: false + jsdom: true + kjua: true # http://eslint.org/docs/rules/ rules: @@ -66,7 +75,6 @@ rules: no-case-declarations: 2 no-div-regex: 2 no-else-return: 0 - no-empty-label: 2 no-empty-pattern: 2 no-eq-null: 2 no-eval: 2 @@ -91,7 +99,7 @@ rules: no-octal-escape: 2 no-octal: 2 no-proto: 2 - no-redeclare: 2 + no-redeclare: 0 no-return-assign: 2 no-script-url: 2 no-self-compare: 2 @@ -187,7 +195,9 @@ rules: operator-linebreak: 0 padded-blocks: 0 quote-props: 0 - quotes: 0 + quotes: + - error + - single require-jsdoc: 0 semi-spacing: 0 semi: 0 diff --git a/.gitattributes b/.gitattributes index daef8b1..3c39546 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,8 +2,6 @@ doc/ export-ignore tst/ export-ignore js/.istanbul.yml export-ignore js/test.js export-ignore -js/mocha-3.2.0.js export-ignore -css/mocha-3.2.0.css export-ignore .codeclimate.yml export-ignore .csslintrc export-ignore .dockerignore export-ignore diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f8363e8..55fbca6 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -35,5 +35,4 @@ If you have access to the server log files, also copy them here. **PrivateBin version:** -* I can reproduce this issue on : Yes / No - +I can reproduce this issue on : Yes / No diff --git a/.gitignore b/.gitignore index c17e3b4..eb61b17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Ignore server files for safety .htaccess .htpasswd -cfg/conf.ini +cfg/conf.php # Ignore data/ data/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..fabd7e6 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,46 @@ +{ + "bitwise": true, + "curly": true, + "eqeqeq": true, + "esversion": 5, + "forin": true, + "freeze": true, + "futurehostile": true, + "latedef": "nofunc", + "maxcomplexity": 25, + "maxdepth": 3, + "maxparams": 4, + "maxstatements": 100, + "noarg": true, + "nonbsp": true, + "nonew": true, + "quotmark": "single", + "singleGroups": true, + "strict": true, + "undef": true, + "unused": true, + "jquery": true, + "browser": true, + "predef": { + "after": true, + "before": true, + "cleanup": true, + "console": true, + "describe": false, + "document": true, + "fs": false, + "global": true, + "exports": true, + "it": false, + "jsc": false, + "jsdom": true, + "require": false, + "setTimeout": false, + "window": true + }, + "globals": { + "sjcl": true, + "DOMPurify": true, + "kjua": true + } +} diff --git a/.nsprc b/.nsprc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.nsprc @@ -0,0 +1 @@ +{} diff --git a/.styleci.yml b/.styleci.yml index 002616b..8a62bd5 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -3,14 +3,17 @@ preset: recommended risky: false enabled: - - no_empty_comment - align_equals - - long_array_syntax - concat_with_spaces + - long_array_syntax + - no_empty_comment + - pre_increment disabled: - blank_line_after_opening_tag - blank_line_before_return + - blank_line_before_throw + - blank_line_before_try - concat_without_spaces - declare_equal_normalize - heredoc_to_nowdoc @@ -21,6 +24,7 @@ disabled: - phpdoc_separation - phpdoc_single_line_var_spacing - phpdoc_summary + - post_increment - short_array_syntax - single_line_after_imports - unalign_equals diff --git a/.travis.yml b/.travis.yml index 7998718..d368a0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,23 +6,30 @@ php: - '5.6' - '7.0' - '7.1' + - '7.2' # 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 + - if [ ! -d "$HOME/.nvm" ]; then mkdir -p $HOME/.nvm && curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | NVM_METHOD=script bash; fi + - 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 .. + - cd js && npm install jsverify jsdom@9 jsdom-global@2 mime-types script: - - cd tst && ../vendor/bin/phpunit - - cd ../js && mocha + - mocha + - cd ../tst && ../vendor/bin/phpunit after_script: - - cd .. - - vendor/bin/codacycoverage clover tst/log/coverage-clover.xml - - vendor/bin/test-reporter --coverage-report tst/log/coverage-clover.xml + - ../vendor/bin/test-reporter --coverage-report log/coverage-clover.xml + - cd .. && vendor/bin/codacycoverage clover tst/log/coverage-clover.xml + +cache: + directories: + - $HOME/.composer/cache/files + - $HOME/.composer/cache/vcs + - $HOME/.nvm + - $HOME/.npm + - js/node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 964d4ee..8e77d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * CHANGED: Minimum required PHP version is 5.4 (#186) * CHANGED: Shipped .htaccess files were updated for Apache 2.4 (#192) * CHANGED: Cleanup of bootstrap template variants and moved icons to `img` directory + * **1.1.1 (2017-10-06)** + * CHANGED: Switched to `.php` file extension for configuration file, to avoid leaking configuration data in unprotected installation. * **1.1 (2016-12-26)** * ADDED: Translations for Italian and Russian * ADDED: Loading message displayed until decryption succeeded for slower (in terms of CPU or network) systems diff --git a/Dockerfile b/Dockerfile index b012134..7bc9e12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,24 @@ FROM php:apache RUN apt-get update && apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ - libpng12-dev \ + libpng-dev \ wget \ zip \ - unzip; \ + unzip && \ # We install and enable php-gd - docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; \ - docker-php-ext-install -j$(nproc) gd; \ - + docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ &&\ + docker-php-ext-install -j$(nproc) gd && \ # We enable Apache's mod_rewrite a2enmod rewrite -COPY . . + +# Copy app content +COPY . /var/www/html + +# Copy start script +RUN mv /var/www/html/docker/entrypoint.sh / && \ + rm -r /var/www/html/docker + +VOLUME /var/www/html/data + +CMD /entrypoint.sh diff --git a/INSTALL.md b/INSTALL.md index a4bb08c..29dc7f1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,12 +3,13 @@ **TL;DR:** Download the [latest release archive](https://github.com/PrivateBin/PrivateBin/releases/latest) and extract it in your web hosts folder where you want to install your PrivateBin -instance. We try to provide a safe default configuration, but we advise you to -check the options and adjust them as you see fit. +instance. We try to provide a mostly safe default configuration, but we urge you to +check the [security section](#hardening-and-security) below and the [configuration +options](#configuration) to adjust as you see fit. -## Basic installation +**NOTE:** See [our FAQ](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#how-can-i-securely-clonedownload-your-project) for information how to securely download the PrivateBin release files. -### Requirements +### Minimal requirements - PHP version 5.4 or above - _one_ of the following sources of cryptographically safe randomness is required: @@ -20,37 +21,11 @@ check the options and adjust them as you see fit. Mcrypt needs to be able to access `/dev/urandom`. This means if `open_basedir` is set, it must include this file. - GD extension -- some disk space or (optional) a database supported by [PDO](https://secure.php.net/manual/book.pdo.php) -- ability to create files and folders in the installation directory and the PATH +- some disk space or (optionally) a database supported by [PDO](https://secure.php.net/manual/book.pdo.php) +- ability to create files and folders in the installation directory and the PATH defined in index.php - A web browser with javascript support -### Configuration - -In the file `cfg/conf.ini` you can configure PrivateBin. A `cfg/conf.ini.sample` -is provided containing all options and default values. You can copy it to -`cfg/conf.ini` and adapt it as needed. The config file is divided into multiple -sections, which are enclosed in square brackets. - -In the `[main]` section you can enable or disable the discussion feature, set -the limit of stored pastes and comments in bytes. The `[traffic]` section lets -you set a time limit in seconds. Users may not post more often then this limit -to your PrivateBin installation. - -More details can be found in the -[configuration documentation](https://github.com/PrivateBin/PrivateBin/wiki/Configuration). - -## Further configuration - -After (or before) setting up PrivateBin, also set up HTTPS, as without HTTPS -PrivateBin is not secure. ( -[More information](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#how-should-i-setup-https)) - -If you want to use PrivateBin behind Cloudflare, make sure you disabled Rocket -loader and unchecked "Javascript" for Auto Minify, found in your domain settings, -under "Speed". (More information -[in this FAQ entry](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#user-content-how-to-make-privatebin-work-when-using-cloudflare-for-ddos-protection)) - -## Advanced installation +## Hardening and security ### Changing the path @@ -75,6 +50,29 @@ process (see also > PrivateBin will look for your includes / data here: > /home/example.com/secret/privatebin +### Transport security + +When setting up PrivateBin, also set up HTTPS, if you haven't already. Without HTTPS +PrivateBin is not secure, as the javascript files could be manipulated during transmission. +For more information on this, see our [FAQ entry on HTTPS setup](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#how-should-i-setup-https). + +## Configuration + +In the file `cfg/conf.php` you can configure PrivateBin. A `cfg/conf.sample.php` +is provided containing all options and default values. You can copy it to +`cfg/conf.php` and adapt it as needed. The config file is divided into multiple +sections, which are enclosed in square brackets. + +In the `[main]` section you can enable or disable the discussion feature, set +the limit of stored pastes and comments in bytes. The `[traffic]` section lets +you set a time limit in seconds. Users may not post more often then this limit +to your PrivateBin installation. + +More details can be found in the +[configuration documentation](https://github.com/PrivateBin/PrivateBin/wiki/Configuration). + +## Advanced installation + ### Web server configuration A `robots.txt` file is provided in the root dir of PrivateBin. It disallows all @@ -88,6 +86,13 @@ some known robots and link-scanning bots. If you use Apache, you can rename the file to `.htaccess` to enable this feature. If you use another webserver, you have to configure it manually to do the same. +### On using Cloudflare + +If you want to use PrivateBin behind Cloudflare, make sure you have disabled the Rocket +loader and unchecked "Javascript" for Auto Minify, found in your domain settings, +under "Speed". (More information +[in this FAQ entry](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#user-content-how-to-make-privatebin-work-when-using-cloudflare-for-ddos-protection)) + ### Using a database instead of flat files In the configuration file the `[model]` and `[model_options]` sections let you @@ -118,32 +123,36 @@ For reference or if you want to create the table schema for yourself (replace `prefix_` with your own table prefix and create the table schema with phpMyAdmin or the MYSQL console): - CREATE TABLE prefix_paste ( - dataid CHAR(16) NOT NULL, - data BLOB, - postdate INT, - expiredate INT, - opendiscussion INT, - burnafterreading INT, - meta TEXT, - attachment MEDIUMBLOB, - attachmentname BLOB, - PRIMARY KEY (dataid) - ); - - CREATE TABLE prefix_comment ( - dataid CHAR(16), - pasteid CHAR(16), - parentid CHAR(16), - data BLOB, - nickname BLOB, - vizhash BLOB, - postdate INT, - PRIMARY KEY (dataid) - ); - CREATE INDEX parent ON prefix_comment(pasteid); - - CREATE TABLE prefix_config ( - id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id) - ); - INSERT INTO prefix_config VALUES('VERSION', '1.1'); +```sql +CREATE TABLE prefix_paste ( + dataid CHAR(16) NOT NULL, + data BLOB, + postdate INT, + expiredate INT, + opendiscussion INT, + burnafterreading INT, + meta TEXT, + attachment MEDIUMBLOB, + attachmentname BLOB, + PRIMARY KEY (dataid) +); + +CREATE TABLE prefix_comment ( + dataid CHAR(16), + pasteid CHAR(16), + parentid CHAR(16), + data BLOB, + nickname BLOB, + vizhash BLOB, + postdate INT, + PRIMARY KEY (dataid) +); +CREATE INDEX parent ON prefix_comment(pasteid); + +CREATE TABLE prefix_config ( + id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id) +); +INSERT INTO prefix_config VALUES('VERSION', '1.1'); +``` + +In PostgreSQL, the attachment column needs to be TEXT and not BLOB or MEDIUMBLOB. diff --git a/LICENSE.md b/LICENSE.md index 05a4125..8492fc3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -30,6 +30,16 @@ the following restrictions: 3. This notice may not be removed or altered from any source distribution. +### MIT license for kjua + +Copyright (c) 2016 Lars Jung (https://larsjung.de) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ## GNU General Public License, version 2.0, for SJCL, rawdeflate and rawinflate _Version 2, June 1991_ diff --git a/README.md b/README.md index a259cb5..b24dc31 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,18 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/094500f62abf4c9aa0c8a8a4520e4789)](https://www.codacy.com/app/PrivateBin/PrivateBin) [![Test Coverage](https://codeclimate.com/github/PrivateBin/PrivateBin/badges/coverage.svg)](https://codeclimate.com/github/PrivateBin/PrivateBin/coverage) [![Code Coverage](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/?branch=master) -*Current version: 1.1* +*Current version: 1.1.1* -**PrivateBin** is a minimalist, open source online pastebin where the server has -zero knowledge of pasted data. +**PrivateBin** is a minimalist, open source online [pastebin](https://en.wikipedia.org/wiki/Pastebin) +where the server has zero knowledge of pasted data. -Data is encrypted/decrypted in the browser using 256bit AES in [Galois Counter mode](https://en.wikipedia.org/wiki/Galois/Counter_Mode). +Data is encrypted and decrypted in the browser using 256bit AES in [Galois Counter mode](https://en.wikipedia.org/wiki/Galois/Counter_Mode). This is a fork of ZeroBin, originally developed by -[Sébastien Sauvage](https://github.com/sebsauvage/ZeroBin). It was refactored -to allow easier and cleaner extensions and has now many more features than the -original. It is however still fully compatible to the original ZeroBin 0.19 -data storage scheme. Therefore such installations can be upgraded to this fork +[Sébastien Sauvage](https://github.com/sebsauvage/ZeroBin). ZeroBin was refactored +to allow easier and cleaner extensions. PrivateBin has many more features than the +original ZeroBin. It is, however, still fully compatible to the original ZeroBin 0.19 +data storage scheme. Therefore, such installations can be upgraded to PrivateBin without losing any data. ## What PrivateBin provides @@ -38,37 +38,37 @@ without losing any data. ## What it doesn't provide -- As a user you have to trust the server administrator, your internet provider - and any country the traffic passes not to inject any malicious javascript code. - For a basic security the PrivateBin installation *has to provide HTTPS*! - Additionally it should be secured by +- As a user you have to trust the server administrator not to inject any malicious + javascript code. + For basic security, the PrivateBin installation *has to provide HTTPS*! + Otherwise you would also have to trust your internet provider, and any country + the traffic passes through. + Additionally the instance should be secured by [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) and ideally by [HPKP](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning) using a - certificate either validated by a trusted third party (check the certificate - when first using a new PrivateBin instance) or self-signed by the server - operator, validated using a + certificate. It can use traditional certificate authorities and/or use [DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions) protected [DANE](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record. - The "key" used to encrypt the paste is part of the URL. If you publicly post - the URL of a paste that is not password-protected, everybody can read it. - Use a password if you want your paste to be private. In this case make sure to - use a strong password and do only share it privately and end-to-end-encrypted. + the URL of a paste that is not password-protected, anyone can read it. + Use a password if you want your paste to be private. In this case, make sure to + use a strong password and only share it privately and end-to-end-encrypted. - A server admin might be forced to hand over access logs to the authorities. - PrivateBin encrypts your text and the discussion contents, but who accessed it - first might still be disclosed via such access logs. + PrivateBin encrypts your text and the discussion contents, but who accessed a + paste (first) might still be disclosed via access logs. - In case of a server breach your data is secure as it is only stored encrypted - on the server. However the server could be misused or the server admin could + on the server. However, the server could be misused or the server admin could be legally forced into sending malicious JavaScript to all web users, which - grabs the decryption key and send it to the server when a user accesses a + grabs the decryption key and sends it to the server when a user accesses a PrivateBin. - Therefore do not access any PrivateBin instance if you think it has been + Therefore, do not access any PrivateBin instance if you think it has been compromised. As long as no user accesses this instance with a previously - generated URL, the content can''t be decrypted. + generated URL, the content can't be decrypted. ## Options diff --git a/cfg/conf.ini.sample b/cfg/conf.sample.php similarity index 93% rename from cfg/conf.ini.sample rename to cfg/conf.sample.php index 0d251c1..145529a 100644 --- a/cfg/conf.ini.sample +++ b/cfg/conf.sample.php @@ -1,3 +1,4 @@ +;this FAQ for information to troubleshoot.": "En caso de que este mensaje nunca desaparezca por favor revise este FAQ para obtener información para solucionar problemas.", - "+++ no paste text +++": "+++ no paste text +++" + "+++ no paste text +++": "+++ sin texto +++" } diff --git a/i18n/fr.json b/i18n/fr.json index 10c36a4..bcfc43c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1,7 +1,7 @@ { "PrivateBin": "PrivateBin", "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bits AES. More information on the project page.": - "%s est un 'pastebin' (ou gestionnaire d'extraits de texte et de code source) minimaliste et open source, dans lequel le serveur n'a aucune connaissance des données envoyées. Les données sont chiffrées/déchiffrées dans le navigateur par un chiffrage AES 256 bits. Plus d'informations sur la page du projet.", + "%s est un 'pastebin' (ou gestionnaire d'extraits de texte et de code source) minimaliste et open source, dans lequel le serveur n'a aucune connaissance des données envoyées. Les données sont chiffrées/déchiffrées dans le navigateur par un chiffrement AES 256 bits. Plus d'informations sur la page du projet.", "Because ignorance is bliss": "Parce que l'ignorance c'est le bonheur", "en": "fr", @@ -83,7 +83,7 @@ "Could not decrypt data (Wrong key?)": "Impossible de déchiffrer les données (mauvaise clé ?)", "Could not delete the paste, it was not stored in burn after reading mode.": - "Impossible de supprimer le paste, car il n'a pas été stoclé en mode \"Effacer après lecture\".", + "Impossible de supprimer le paste, car il n'a pas été stocké en mode \"Effacer après lecture\".", "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "POUR VOS YEUX UNIQUEMENT. Ne fermez pas cette fenêtre, ce paste ne pourra plus être affiché.", "Could not decrypt comment; Wrong key?": @@ -93,7 +93,7 @@ "Anonymous": "Anonyme", "Avatar generated from IP address": - "Avatar anonyme (Vizhash de l'adresse IP)", + "Avatar généré à partir de l'adresse IP", "Add comment": "Ajouter un commentaire", "Optional nickname…": @@ -123,7 +123,7 @@ "Could not create paste: %s": "Impossible de créer le paste : %s", "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": - "Impossible de déchiffrer le paste : Clé de déchiffrage manquante dans l'URL (Avez-vous utilisé un redirecteur ou un site de réduction d'URL qui supprime une partie de l'URL ?)", + "Impossible de déchiffrer le paste : Clé de déchiffrement manquante dans l'URL (Avez-vous utilisé un redirecteur ou un site de réduction d'URL qui supprime une partie de l'URL ?)", "B": "o", "KiB": "Kio", "MiB": "Mio", @@ -139,8 +139,10 @@ "Markdown": "Markdown", "Download attachment": "Télécharger la pièce jointe", "Cloned: '%s'": "Cloner '%s'", - "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", + "The cloned file '%s' was attached to this paste.": "Le fichier cloné '%s' a été attaché à ce paste.", "Attach a file": "Attacher un fichier ", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Enlever l'attachement", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Votre navigateur ne supporte pas l'envoi de fichiers chiffrés. Merci d'utiliser un navigateur plus récent.", @@ -156,9 +158,9 @@ "Enter password": "Entrez le mot de passe", "Loading…": "Chargement…", - "Decrypting paste…": "Decrypting paste…", - "Preparing new paste…": "Preparing new paste…", + "Decrypting paste…": "Déchiffrement du paste…", + "Preparing new paste…": "Préparation du paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Si ce message ne disparaîssait pas, jetez un oeil à cette FAQ pour des idées de résolution (en Anglais).", - "+++ no paste text +++": "+++ no paste text +++" + "+++ no paste text +++": "+++ pas de paste-text +++" } diff --git a/i18n/it.json b/i18n/it.json index d7885d7..bf15fc1 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -132,6 +132,8 @@ "Cloned: '%s'": "Clonato: '%s'", "The cloned file '%s' was attached to this paste.": "Il file clonato '%s' era allegato a questo messaggio.", "Attach a file": "Allega un file", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Rimuovi allegato", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Il tuo browser non supporta l'invio di file cifrati. Utilizza un browser più recente.", diff --git a/i18n/no.json b/i18n/no.json index 8292e10..912cfc0 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -132,6 +132,8 @@ "Cloned: '%s'": "Kopiert: '%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", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Slett vedlegg", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Nettleseren din støtter ikke å laste opp krypterte filer. Vennligst bruk en nyere nettleser.", diff --git a/i18n/oc.json b/i18n/oc.json index 9047876..482a031 100644 --- a/i18n/oc.json +++ b/i18n/oc.json @@ -1,9 +1,9 @@ { "PrivateBin": "PrivateBin", "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bits AES. More information on the project page.": - "%s es un 'pastebin' (o gestionari d'extrachs de tèxte e còdi font) minimalista e open source, dins lo qual lo servidor a pas cap de coneissença de las donadas mandadas. Las donadas son chifradas/deschifradas dins lo navigator per un chiframent AES 256 bits. Mai informacions sus la pagina del projècte.", + "%s es un 'pastebin' (o gestionari d’extrachs de tèxte e còdi font) minimalista e open source, dins lo qual lo servidor a pas cap de coneissença de las donadas mandadas. Las donadas son chifradas/deschifradas dins lo navigator per un chiframent AES 256 bits. Mai informacions sus la pagina del projècte.", "Because ignorance is bliss": - "Perque l'ignorància es bonaür", + "Perque lo bonaür es l’ignorància", "en": "oc", "Paste does not exist, has expired or has been deleted.": "Lo tèxte existís pas, a expirat, o es estat suprimit.", @@ -32,11 +32,11 @@ "Paste was properly deleted.": "Lo tèxte es estat correctament suprimit.", "JavaScript is required for %s to work.
Sorry for the inconvenience.": - "JavaScript es requesit per far foncionar %s.
O planhèm per l'inconvenient.", + "JavaScript es requesit per far foncionar %s.
O planhèm per l’inconvenient.", "%s requires a modern browser to work.": "%s necessita un navigator modèrn per foncionar.", "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": - "Encora sus Internet Explorer ? Fasètz-vos una favor, passatz a un navigator modèrn :", + "Encora sus Internet Explorer ? Fasètz-vos una favor, passatz a un navigator modèrn :", "New": "Nòu", "Send": @@ -67,7 +67,7 @@ "Never": "Jamai", "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": - "Nota : Aquò es un servici d'espròva : las donadas pòdon èsser suprimidas a cada moment. De catons moriràn s'abusatz d'aqueste servici.", + "Nota : Aquò es un servici d’espròva : las donadas pòdon èsser suprimidas a cada moment. De catons moriràn s’abusatz d’aqueste servici.", "This document will expire in %d seconds.": ["Ce document expirera dans %d seconde.", "Aqueste document expirarà dins %d segondas."], "This document will expire in %d minutes.": @@ -79,21 +79,21 @@ "This document will expire in %d months.": ["Ce document expirera dans %d mois.", "Aqueste document expirarà dins %d meses."], "Please enter the password for this paste:": - "Picatz lo senhal per aqueste tèxte :", + "Picatz lo senhal per aqueste tèxte :", "Could not decrypt data (Wrong key?)": - "Impossible de deschifrar las donadas (marrida clau ?)", + "Impossible de deschifrar las donadas (marrida clau ?)", "Could not delete the paste, it was not stored in burn after reading mode.": "Impossible de suprimir lo tèxte, perque es pas estat gardat en mòde \"Escafar aprèp lectura\".", "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "PER VÒSTRES UÈLHS SOLAMENT. Tampetz pas aquesta fenèstra, aqueste tèxte poirà pas mai èsser afichat.", "Could not decrypt comment; Wrong key?": - "Impossible de deschifrar lo comentari ; marrida clau ?", + "Impossible de deschifrar lo comentari ; marrida clau ?", "Reply": "Respondre", "Anonymous": "Anonime", "Avatar generated from IP address": - "Avatar anonime (Vizhash de l'adreça IP)", + "Avatar anonime (Vizhash de l’adreça IP)", "Add comment": "Apondre un comentari", "Optional nickname…": @@ -105,25 +105,25 @@ "Comment posted.": "Comentari mandat.", "Could not refresh display: %s": - "Impossible d'actualizar l'afichatge : %s", + "Impossible d’actualizar l’afichatge : %s", "unknown status": "Estatut desconegut", "server error or not responding": "Lo servidor respond pas o a rencontrat una error", "Could not post comment: %s": - "Impossible de mandar lo comentari : %s", + "Impossible de mandar lo comentari : %s", "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 %s (Hit [Ctrl]+[c] to copy)": - "Vòstre tèxte es disponible a l'adreça %s (Picatz sus [Ctrl]+[c] per copiar)", + "Vòstre tèxte es disponible a l’adreça %s (Picatz sus [Ctrl]+[c] per copiar)", "Delete data": "Supprimir las donadas del tèxte", "Could not create paste: %s": - "Impossible de crear lo tèxte : %s", + "Impossible de crear lo tèxte : %s", "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": - "Impossible de deschifrar lo tèxte : Clau de deschiframent absenta de l'URL (Avètz utilizat un redirector o un site de reduccion d'URL que suprimís una partida de l'URL ?)", + "Impossible de deschifrar lo tèxte : clau de deschiframent absenta de l’URL (Avètz utilizat un redirector o un site de reduccion d’URL que suprimís una partida de l’URL ?)", "B": "o", "KiB": "Kio", "MiB": "Mio", @@ -138,15 +138,17 @@ "Source Code": "Còdi font", "Markdown": "Markdown", "Download attachment": "Telecargar la pèça junta", - "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", + "Cloned: '%s'": "Clonar : '%s'", + "The cloned file '%s' was attached to this paste.": "Aqueste fichièr clonat '%s' es estat ajustat a aqueste tèxte.", + "Attach a file": "Juntar un fichièr", + "alternatively drag & drop a file or paste an image from the clipboard": "autrament lisatz lo fichièr o pegatz l’imatge del quichapapièrs", + "File too large, to display a preview. Please download the attachment.": "Fichièr tròp pesuc per mostrar un apercebut. Telecargatz la pèca junta.", + "Remove attachment": "Levar la pèça junta", "Your browser does not support uploading encrypted files. Please use a newer browser.": - "Vòstre navigator es pas compatible amb lo mandadís de fichièrs chifrats. Mercés d'emplegar un navigator mai recent.", + "Vòstre navigator es pas compatible amb lo mandadís de fichièrs chifrats. Mercés d’emplegar un navigator mai recent.", "Invalid attachment.": "Pèça junta invalida.", "Options": "Opcions", - "Shorten URL": "Acorchir l'URL", + "Shorten URL": "Acorchir l’URL", "Editor": "Editar", "Preview": "Previsualizar", "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": @@ -156,9 +158,9 @@ "Enter password": "Picatz lo senhal", "Loading…": "Cargament…", - "Decrypting paste…": "Decrypting paste…", - "Preparing new paste…": "Preparing new paste…", + "Decrypting paste…": "Deschirament del tèxte…", + "Preparing new paste…": "Preparacion…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar aquesta FAQ per las solucions (en Anglés).", - "+++ no paste text +++": "+++ no paste text +++" + "Se per cas aqueste messatge quita pas de s’afichar mercés de gaitar aquesta FAQ per las solucions (en anglés).", + "+++ no paste text +++": "+++ cap de tèxte pegat +++" } diff --git a/i18n/pl.json b/i18n/pl.json index 82d9b57..9842246 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -132,6 +132,8 @@ "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", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Usuń załącznik", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Twoja przeglądarka nie wspiera wysyłania zaszyfrowanych plików. Użyj nowszej przeglądarki.", diff --git a/i18n/pt.json b/i18n/pt.json index 05ce23d..5eb5b4a 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -132,6 +132,8 @@ "Cloned: '%s'": "Clonado: '%s'", "The cloned file '%s' was attached to this paste.": "O arquivo clonado '%s' foi anexado a essa cópia.", "Attach a file": "Anexar um arquivo", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Remover anexo", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Seu navegador não permite subir arquivos cifrados. Por favor, utilize um navegador mais recente.", diff --git a/i18n/ru.json b/i18n/ru.json index da462c3..8324723 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -142,6 +142,8 @@ "The cloned file '%s' was attached to this paste.": "Дубликат файла '%s' был прикреплен к этой записи.", "Attach a file": "Прикрепить файл", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Удалить вложение", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Ваш браузер не поддерживает отправку зашифрованных файлов. Используйте более новый браузер.", diff --git a/i18n/sl.json b/i18n/sl.json index 21db8c1..fc67eac 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -141,6 +141,8 @@ "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", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "Odstrani priponko", "Your browser does not support uploading encrypted files. Please use a newer browser.": "Tvoj brskalnik ne omogoča nalaganje zakodiranih datotek. Prosim uporabi novejši brskalnik.", diff --git a/i18n/zh.json b/i18n/zh.json index 5fcaf3d..a41b6de 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -132,6 +132,8 @@ "Cloned: '%s'": "克隆: '%s'", "The cloned file '%s' was attached to this paste.": "克隆文件 '%s' 已附加到此粘贴。", "Attach a file": "添加一个附件", + "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard", + "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.", "Remove attachment": "移除附件", "Your browser does not support uploading encrypted files. Please use a newer browser.": "您的浏览器不支持上传加密的文件,请使用更新的浏览器。", diff --git a/img/icon_qr.png b/img/icon_qr.png new file mode 100644 index 0000000..28d10ca Binary files /dev/null and b/img/icon_qr.png differ diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..3ab4999 --- /dev/null +++ b/js/common.js @@ -0,0 +1,154 @@ +'use strict'; + +// testing prerequisites +global.assert = require('assert'); +global.jsc = require('jsverify'); +global.jsdom = require('jsdom-global'); +global.cleanup = global.jsdom(); +global.fs = require('fs'); + +// application libraries to test +global.$ = global.jQuery = require('./jquery-3.1.1'); +global.sjcl = require('./sjcl-1.0.6'); +global.Base64 = require('./base64-2.1.9').Base64; +global.RawDeflate = require('./rawdeflate-0.5').RawDeflate; +global.RawDeflate.inflate = require('./rawinflate-0.3').RawDeflate.inflate; +require('./prettify'); +global.prettyPrint = window.PR.prettyPrint; +global.prettyPrintOne = window.PR.prettyPrintOne; +global.showdown = require('./showdown-1.6.1'); +global.DOMPurify = require('./purify-1.0.3'); +require('./bootstrap-3.3.7'); +require('./privatebin'); + +// internal variables +var a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m', + 'n','o','p','q','r','s','t','u','v','w','x','y','z'], + alnumString = a2zString.concat(['0','1','2','3','4','5','6','7','8','9']), + queryString = alnumString.concat(['+','%','&','.','*','-','_']), + base64String = alnumString.concat(['+','/','=']).concat( + a2zString.map(function(c) { + return c.toUpperCase(); + }) + ), + schemas = ['ftp','gopher','http','https','ws','wss'], + supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], + mimeTypes = ['image/png', 'application/octet-stream'], + formats = ['plaintext', 'markdown', 'syntaxhighlighting'], + /** + * character to HTML entity lookup table + * + * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} + */ + entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }, + logFile = fs.createWriteStream('test.log'), + mimeFile = fs.createReadStream('/etc/mime.types'), + mimeLine = ''; + +// redirect console messages to log file +console.info = console.warn = console.error = function () { + logFile.write(Array.prototype.slice.call(arguments).join('') + '\n'); +}; + +// populate mime types from environment +mimeFile.on('data', function(data) { + mimeLine += data; + var index = mimeLine.indexOf('\n'); + while (index > -1) { + var line = mimeLine.substring(0, index); + mimeLine = mimeLine.substring(index + 1); + parseMime(line); + index = mimeLine.indexOf('\n'); + } +}); + +mimeFile.on('end', function() { + if (mimeLine.length > 0) { + parseMime(mimeLine); + } +}); + +function parseMime(line) { + // ignore comments + var index = line.indexOf('#'); + if (index > -1) { + line = line.substring(0, index); + } + + // ignore bits after tabs + index = line.indexOf('\t'); + if (index > -1) { + line = line.substring(0, index); + } + if (line.length > 0) { + mimeTypes.push(line); + } +} + +// common testing helper functions + +/** + * convert all applicable characters to HTML entities + * + * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} + * @name htmlEntities + * @function + * @param {string} str + * @return {string} escaped HTML + */ +exports.htmlEntities = function(str) { + return String(str).replace( + /[&<>"'`=\/]/g, function(s) { + return entityMap[s]; + }); +}; + +// provides random lowercase characters from a to z +exports.jscA2zString = function() { + return jsc.elements(a2zString); +}; + +// provides random lowercase alpha numeric characters (a to z and 0 to 9) +exports.jscAlnumString = function() { + return jsc.elements(alnumString); +}; + +// provides random characters allowed in GET queries +exports.jscQueryString = function() { + return jsc.elements(queryString); +}; + +// provides random characters allowed in base64 encoded strings +exports.jscBase64String = function() { + return jsc.elements(base64String); +}; + +// provides a random URL schema supported by the whatwg-url library +exports.jscSchemas = function() { + return jsc.elements(schemas); +}; + +// provides a random supported language string +exports.jscSupportedLanguages = function() { + return jsc.elements(supportedLanguages); +}; + +// provides a random mime type +exports.jscMimeTypes = function() { + return jsc.elements(mimeTypes); +}; + +// provides a random PrivateBin paste formatter +exports.jscFormats = function() { + return jsc.elements(formats); +}; + diff --git a/js/kjua-0.1.2.js b/js/kjua-0.1.2.js new file mode 100644 index 0000000..9325787 --- /dev/null +++ b/js/kjua-0.1.2.js @@ -0,0 +1,2 @@ +/*! kjua v0.1.2 - https://larsjung.de/kjua/ */ +!function(r,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.kjua=t():r.kjua=t()}(this,function(){return function(r){function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return r[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t,e){"use strict";var n=e(1),o=n.createCanvas,i=n.canvasToImg,a=n.dpr,u=e(2),f=e(3),c=e(4);r.exports=function(r){var t=Object.assign({},u,r),e=f(t.text,t.ecLevel,t.minVersion,t.quiet),n=t.ratio||a,l=o(t.size,n),s=l.getContext("2d");return s.scale(n,n),c(e,s,t),"image"===t.render?i(l):l}},function(r,t){"use strict";var e=window,n=e.document,o=e.devicePixelRatio||1,i=function(r){return n.createElement(r)},a=function(r,t){return r.getAttribute(t)},u=function(r,t,e){return r.setAttribute(t,e)},f=function(r,t){var e=i("canvas");return u(e,"width",r*t),u(e,"height",r*t),e.style.width=r+"px",e.style.height=r+"px",e},c=function(r){var t=i("img");return u(t,"crossorigin","anonymous"),u(t,"src",r.toDataURL("image/png")),u(t,"width",a(r,"width")),u(t,"height",a(r,"height")),t.style.width=r.style.width,t.style.height=r.style.height,t};r.exports={createCanvas:f,canvasToImg:c,dpr:o}},function(r,t){"use strict";r.exports={render:"image",crisp:!0,minVersion:1,ecLevel:"L",size:200,ratio:null,fill:"#333",back:"#fff",text:"no text",rounded:0,quiet:0,mode:"plain",mSize:30,mPosX:50,mPosY:50,label:"no label",fontname:"sans",fontcolor:"#333",image:null}},function(r,t){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(r){return typeof r}:function(r){return r&&"function"==typeof Symbol&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},n=/code length overflow/i,o=function(){var e=function(){function r(t,e){if("undefined"==typeof t.length)throw new Error(t.length+"/"+e);var n=function(){for(var r=0;r=7&&T(r),null==d&&(d=x(l,s,w)),b(d,t)},m=function(r,t){for(var e=-1;e<=7;e+=1)if(!(r+e<=-1||h<=r+e))for(var n=-1;n<=7;n+=1)t+n<=-1||h<=t+n||(0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4?g[r+e][t+n]=!0:g[r+e][t+n]=!1)},A=function(){for(var r=0,t=0,e=0;e<8;e+=1){p(!0,e);var n=i.getLostPoint(y);(0==e||r>n)&&(r=n,t=e)}return t},B=function(){for(var r=8;r>e&1);g[Math.floor(e/3)][e%3+h-8-3]=n}for(var e=0;e<18;e+=1){var n=!r&&1==(t>>e&1);g[e%3+h-8-3][Math.floor(e/3)]=n}},M=function(r,t){for(var e=s<<3|t,n=i.getBCHTypeInfo(e),o=0;o<15;o+=1){var a=!r&&1==(n>>o&1);o<6?g[o][8]=a:o<8?g[o+1][8]=a:g[h-15+o][8]=a}for(var o=0;o<15;o+=1){var a=!r&&1==(n>>o&1);o<8?g[8][h-o-1]=a:o<9?g[8][15-o-1+1]=a:g[8][15-o-1]=a}g[h-8][8]=!r},b=function(r,t){for(var e=-1,n=h-1,o=7,a=0,u=i.getMaskFunction(t),f=h-1;f>0;f-=2)for(6==f&&(f-=1);;){for(var c=0;c<2;c+=1)if(null==g[n][f-c]){var l=!1;a>>o&1));var s=u(n,f-c);s&&(l=!l),g[n][f-c]=l,o-=1,o==-1&&(a+=1,o=7)}if(n+=e,n<0||h<=n){n-=e,e=-e;break}}},k=function(t,e){for(var n=0,o=0,a=0,u=new Array(e.length),f=new Array(e.length),c=0;c=0?d.getAt(w):0}}for(var y=0,g=0;g8*g)throw new Error("code length overflow. ("+c.getLengthInBits()+">"+8*g+")");for(c.getLengthInBits()+4<=8*g&&c.put(0,4);c.getLengthInBits()%8!=0;)c.putBit(!1);for(;;){if(c.getLengthInBits()>=8*g)break;if(c.put(o,8),c.getLengthInBits()>=8*g)break;c.put(a,8)}return k(c,n)};return y.addData=function(r){var t=c(r);w.push(t),d=null},y.isDark=function(r,t){if(r<0||h<=r||t<0||h<=t)throw new Error(r+","+t);return g[r][t]},y.getModuleCount=function(){return h},y.make=function(){p(!1,A())},y.createTableTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e="";e+='";for(var o=0;o';e+=""}return e+="",e+="
"},y.createImgTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e=y.getModuleCount()*r+2*t,n=t,o=e-t;return v(e,e,function(t,e){if(n<=t&&t>>8),t.push(255&a)):t.push(n)}}return t}};var e={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},n={L:1,M:0,Q:3,H:2},o={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},i=function(){var t=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],n=1335,i=7973,u=21522,f={},c=function(r){for(var t=0;0!=r;)t+=1,r>>>=1;return t};return f.getBCHTypeInfo=function(r){for(var t=r<<10;c(t)-c(n)>=0;)t^=n<=0;)t^=i<5&&(e+=3+i-5)}for(var n=0;n=256;)t-=255;return r[t]},n}(),u=function(){var r=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],t=function(r,t){var e={};return e.totalCount=r,e.dataCount=t,e},e={},o=function(t,e){switch(e){case n.L:return r[4*(t-1)+0];case n.M:return r[4*(t-1)+1];case n.Q:return r[4*(t-1)+2];case n.H:return r[4*(t-1)+3];default:return}};return e.getRSBlocks=function(r,e){var n=o(r,e);if("undefined"==typeof n)throw new Error("bad rs block @ typeNumber:"+r+"/errorCorrectLevel:"+e);for(var i=n.length/3,a=new Array,u=0;u>>7-t%8&1)},e.put=function(r,t){for(var n=0;n>>t-n-1&1))},e.getLengthInBits=function(){return t},e.putBit=function(e){var n=Math.floor(t/8);r.length<=n&&r.push(0),e&&(r[n]|=128>>>t%8),t+=1},e},c=function(r){var n=e.MODE_8BIT_BYTE,o=t.stringToBytes(r),i={};return i.getMode=function(){return n},i.getLength=function(r){return o.length},i.write=function(r){for(var t=0;t>>8)},t.writeBytes=function(r,e,n){e=e||0,n=n||r.length;for(var o=0;o0&&(t+=","),t+=r[e];return t+="]"},t},s=function(){var r=0,t=0,e=0,n="",o={},i=function(r){n+=String.fromCharCode(a(63&r))},a=function(r){if(r<0);else{if(r<26)return 65+r;if(r<52)return 97+(r-26);if(r<62)return 48+(r-52);if(62==r)return 43;if(63==r)return 47}throw new Error("n:"+r)};return o.writeByte=function(n){for(r=r<<8|255&n,t+=8,e+=1;t>=6;)i(r>>>t-6),t-=6},o.flush=function(){if(t>0&&(i(r<<6-t),r=0,t=0),e%3!=0)for(var o=3-e%3,a=0;a=t.length){if(0==o)return-1;throw new Error("unexpected end of file./"+o)}var r=t.charAt(e);if(e+=1,"="==r)return o=0,-1;r.match(/^\s$/)||(n=n<<6|a(r.charCodeAt(0)),o+=6)}var i=n>>>o-8&255;return o-=8,i};var a=function(r){if(65<=r&&r<=90)return r-65;if(97<=r&&r<=122)return r-97+26;if(48<=r&&r<=57)return r-48+52;if(43==r)return 62;if(47==r)return 63;throw new Error("c:"+r)};return i},h=function(r,t){var e=r,n=t,o=new Array(r*t),i={};i.setPixel=function(r,t,n){o[t*e+r]=n},i.write=function(r){r.writeString("GIF87a"),r.writeShort(e),r.writeShort(n),r.writeByte(128),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(255),r.writeByte(255),r.writeByte(255),r.writeString(","),r.writeShort(0),r.writeShort(0),r.writeShort(e),r.writeShort(n),r.writeByte(0);var t=2,o=u(t);r.writeByte(t);for(var i=0;o.length-i>255;)r.writeByte(255),r.writeBytes(o,i,255),i+=255;r.writeByte(o.length-i),r.writeBytes(o,i,o.length-i),r.writeByte(0),r.writeString(";")};var a=function(r){var t=r,e=0,n=0,o={};return o.write=function(r,o){if(r>>>o!=0)throw new Error("length over");for(;e+o>=8;)t.writeByte(255&(r<>>=8-e,n=0,e=0;n|=r<0&&t.writeByte(n)},o},u=function(r){for(var t=1<>6,128|63&n):n<55296||n>=57344?t.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&r.charCodeAt(e)),t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return t}return t(r)}}(e),e}(),i=function(r,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;i=Math.max(1,i);for(var a=i;a<=40;a+=1)try{var u=function(){var e=o(a,t);e.addData(r),e.make();var n=e.getModuleCount(),i=function(r,t){return r>=0&&r=0&&t0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"L",e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,o=i(r,t,e);if(o){var a=o.isDark;o.moduleCount+=2*n,o.isDark=function(r,t){return a(r-n,t-n)}}return o};r.exports=a},function(r,t,e){"use strict";var n=e(5),o=e(6),i=function(r,t){r.fillStyle=t.back,r.fillRect(0,0,t.size,t.size)},a=function(r,t,e,n,o,i){r.isDark(o,i)&&t.rect(i*n,o*n,n,n)},u=function(r,t,e){if(r){var o=e.rounded>0&&e.rounded<=100?n:a,i=r.moduleCount,u=e.size/i,f=0;e.crisp&&(u=Math.floor(u),f=Math.floor((e.size-u*i)/2)),t.translate(f,f),t.beginPath();for(var c=0;c': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; - /** * cache for script location * @@ -116,7 +100,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } v = Math.floor(seconds / (60 * 60 * 24 * 30)); return [v, 'month']; - } + }; /** * text range selection @@ -135,36 +119,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { range = document.body.createTextRange(); range.moveToElementText(element); range.select(); - } else if (window.getSelection){ + } else if (window.getSelection) { selection = window.getSelection(); range = document.createRange(); range.selectNodeContents(element); selection.removeAllRanges(); selection.addRange(range); } - } - - /** - * set text of a jQuery element (required for IE), - * - * @name Helper.setElementText - * @function - * @param {jQuery} $element - a jQuery element - * @param {string} text - the text to enter - */ - me.setElementText = function($element, text) - { - // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this... - if ($('#oldienotice').is(':visible')) { - var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
'); - $element.html('
' + html + '
'); - } - // for other (sane) browsers: - else - { - $element.text(text); - } - } + }; /** * convert URLs to clickable links. @@ -177,24 +139,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name Helper.urls2links * @function - * @param {Object} $element - a jQuery DOM element + * @param {string} html + * @return {string} */ - me.urls2links = function($element) + me.urls2links = function(html) { - var markup = '$1'; - $element.html( - $element.html().replace( - /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, - markup - ) + return html.replace( + /(((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig, + '$1' ); - $element.html( - $element.html().replace( - /((magnet):[\w?=&.\/-;#@~%+*-]+)/ig, - markup - ) - ); - } + }; /** * minimal sprintf emulation for %s and %d formats @@ -232,7 +186,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ++i; return val; }); - } + }; /** * get value of cookie, if it was set, empty string otherwise @@ -258,7 +212,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } return ''; - } + }; /** * get the current location (without search or hash part of the URL), @@ -277,23 +231,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { baseUri = window.location.origin + window.location.pathname; return baseUri; - } - - /** - * convert all applicable characters to HTML entities - * - * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} - * @name Helper.htmlEntities - * @function - * @param {string} str - * @return {string} escaped HTML - */ - me.htmlEntities = function(str) { - return String(str).replace( - /[&<>"'`=\/]/g, function(s) { - return entityMap[s]; - }); - } + }; /** * resets state, used for unit testing @@ -304,7 +242,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.reset = function() { baseUri = null; - } + }; /** * checks whether this is a bot we dislike @@ -314,12 +252,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @return {bool} */ me.isBadBot = function() { - /* - if ($.inArray(navigator.userAgent, BadBotUA) >= 0) { - return true; - } - */ - // check whether a bot user agent part can be found in the current // user agent var arrayLength = BadBotUA.length; @@ -339,8 +271,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * internationalization module * * @name I18n - * @param {object} window - * @param {object} document * @class */ var I18n = (function () { @@ -397,7 +327,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me._ = function() { return me.translate.apply(this, arguments); - } + }; /** * translate a string @@ -434,7 +364,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var usesPlurals = $.isArray(args[0]); if (usesPlurals) { // use the first plural form as messageId, otherwise the singular - messageId = (args[0].length > 1 ? args[0][1] : args[0][0]); + messageId = args[0].length > 1 ? args[0][1] : args[0][0]; } else { messageId = args[0]; } @@ -451,7 +381,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var orgArguments = arguments; $(document).on(languageLoadedEvent, function () { // log to show that the previous error could be mitigated - console.log('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language); + console.warn('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language); // re-execute this function me.translate.apply(this, orgArguments); }); @@ -501,7 +431,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return output; - } + }; /** * per language functions to use to determine the plural form @@ -518,18 +448,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { case 'fr': case 'oc': case 'zh': - return (n > 1 ? 1 : 0); + return n > 1 ? 1 : 0; case 'pl': - return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); + return n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); case 'ru': - return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); + return n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); case 'sl': - return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0))); + return n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)); // de, en, es, it, no, pt default: - return (n !== 1 ? 1 : 0); + return n !== 1 ? 1 : 0; } - } + }; /** * load translations into cache @@ -543,7 +473,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // auto-select language based on browser settings if (newLanguage.length === 0) { - newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2); + newLanguage = (navigator.language || navigator.userLanguage || 'en').substring(0, 2); } // if language is already used skip update @@ -573,7 +503,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); language = 'en'; }); - } + }; /** * resets state, used for unit testing @@ -585,7 +515,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { language = mockLanguage || null; translations = mockTranslations || {}; - } + }; return me; })(); @@ -650,7 +580,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return sjcl.encrypt(key, compress(message), options); } return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options); - } + }; /** * decrypt message with key, then decompress @@ -660,7 +590,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} key * @param {string} password * @param {string} data - JSON with encrypted data - * @return {string} decrypted message + * @return {string} decrypted message, empty if decryption failed */ me.decipher = function(key, password, data) { @@ -671,12 +601,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { try { return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); } catch(e) { - // ignore error, because ????? @TODO + return ''; } } } - return ''; - } + }; /** * checks whether the crypt tool has collected enough entropy @@ -688,7 +617,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isEntropyReady = function() { return sjcl.random.isReady(); - } + }; /** * add a listener function, triggered when enough entropy is available @@ -700,7 +629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.addEntropySeedListener = function(func) { sjcl.random.addEventListener('seeded', func); - } + }; /** * returns a random symmetric key @@ -712,7 +641,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getSymmetricKey = function() { return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0); - } + }; return me; })(); @@ -737,12 +666,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name Model.getExpirationDefault * @function * @return string - * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value) */ me.getExpirationDefault = function() { return $('#pasteExpiration').val(); - } + }; /** * returns the format set in the HTML @@ -750,15 +678,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name Model.getFormatDefault * @function * @return string - * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value) */ me.getFormatDefault = function() { return $('#pasteFormatter').val(); - } + }; /** - * returns the paste data (inlduing the cipher data) + * returns the paste data (including the cipher data) * * @name Model.getPasteData * @function @@ -800,7 +727,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } }) Uploader.run(); - } + }; /** * get the pastes unique identifier from the URL, @@ -822,7 +749,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return id; - } + }; /** * return the deciphering key stored in anchor part of the URL @@ -851,7 +778,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return symmetricKey; - } + }; /** * returns a jQuery copy of the HTML template @@ -867,7 +794,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var $element = $templates.find('#' + name + 'template').clone(true); // change ID to avoid collisions (one ID should really be unique) return $element.prop('id', name); - } + }; /** * resets state, used for unit testing @@ -878,7 +805,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.reset = function() { pasteData = $templates = id = symmetricKey = null; - } + }; /** * init navigation manager @@ -891,7 +818,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { $templates = $('#templates'); - } + }; return me; })(); @@ -902,11 +829,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * everything directly UI-related, which fits nowhere else * * @name UiHelper - * @param {object} window - * @param {object} document * @class */ - var UiHelper = (function (window, document) { + var UiHelper = (function () { var me = {}; /** @@ -923,7 +848,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { var currentLocation = Helper.baseUri(); if (event.originalEvent.state === null && // no state object passed - event.originalEvent.target.location.href === currentLocation && // target location is home page + event.target.location.href === currentLocation && // target location is home page window.location.href === currentLocation // and we are not already on the home page ) { // redirect to home page @@ -942,7 +867,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.reloadHome = function() { window.location.href = Helper.baseUri(); - } + }; /** * checks whether the element is currently visible in the viewport (so @@ -959,8 +884,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var viewportTop = $(window).scrollTop(); var viewportBottom = viewportTop + $(window).height(); - return (elementTop > viewportTop && elementTop < viewportBottom); - } + return elementTop > viewportTop && elementTop < viewportBottom; + }; /** * scrolls to a specific element @@ -1013,7 +938,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } next(); }); - } + }; + + /** + * trigger a history (pop) state change + * + * used to test the UiHelper.historyChange private function + * + * @name UiHelper.mockHistoryChange + * @function + * @param {string} state (optional) state to mock + */ + me.mockHistoryChange = function(state) + { + if (typeof state === 'undefined') { + state = null; + } + historyChange($.Event('popstate', {originalEvent: new PopStateEvent('popstate', {state: state}), target: window})); + }; /** * initialize @@ -1027,10 +969,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $('.reloadlink').prop('href', Helper.baseUri()); $(window).on('popstate', historyChange); - } + }; return me; - })(window, document); + })(); /** * Alert/error manager @@ -1046,12 +988,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $statusMessage, $remainingTime; - var currentIcon = [ - 'glyphicon-time', // loading icon - 'glyphicon-info-sign', // status icon - '', // resevered for warning, not used yet - 'glyphicon-alert' // error icon - ]; + var currentIcon; var alertType = [ 'loading', // not in bootstrap, but using a good value here @@ -1090,7 +1027,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (typeof customHandler === 'function') { var handlerResult = customHandler(alertType[id], $element, args, icon); if (handlerResult === true) { - // if it returs true, skip own handler + // if it returns true, skip own handler return; } if (handlerResult instanceof jQuery) { @@ -1139,20 +1076,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string|array} message string, use an array for %s/%d options * @param {string|null} icon optional, the icon to show, * default: leave previous icon - * @param {bool} dismissable optional, whether the notification - * can be dismissed (closed), default: false - * @param {bool|int} autoclose optional, after how many seconds the - * notification should be hidden automatically; - * default: disabled (0); use true for default value */ - me.showStatus = function(message, icon, dismissable, autoclose) + me.showStatus = function(message, icon) { - console.log('status shown: ', message); - // @TODO: implement dismissable - // @TODO: implement autoclose - + console.info('status shown: ', message); handleNotification(1, $statusMessage, message, icon); - } + }; /** * display an error message @@ -1164,20 +1093,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string|array} message string, use an array for %s/%d options * @param {string|null} icon optional, the icon to show, default: * leave previous icon - * @param {bool} dismissable optional, whether the notification - * can be dismissed (closed), default: false - * @param {bool|int} autoclose optional, after how many seconds the - * notification should be hidden automatically; - * default: disabled (0); use true for default value */ - me.showError = function(message, icon, dismissable, autoclose) + me.showError = function(message, icon) { console.error('error message shown: ', message); - // @TODO: implement dismissable (bootstrap add-on has it) - // @TODO: implement autoclose - handleNotification(3, $errorMessage, message, icon); - } + }; /** * display remaining message @@ -1190,9 +1111,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.showRemaining = function(message) { - console.log('remaining message shown: ', message); + console.info('remaining message shown: ', message); handleNotification(1, $remainingTime, message); - } + }; /** * shows a loading message, optionally with a percentage @@ -1202,13 +1123,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name Alert.showLoading * @function * @param {string|array|null} message optional, use an array for %s/%d options, default: 'Loading…' - * @param {int} percentage optional, default: null * @param {string|null} icon optional, the icon to show, default: leave previous icon */ - me.showLoading = function(message, percentage, icon) + me.showLoading = function(message, icon) { if (typeof message !== 'undefined' && message !== null) { - console.log('status changed: ', message); + console.info('status changed: ', message); } // default message text @@ -1216,14 +1136,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { message = 'Loading…'; } - // currently percentage parameter is ignored - // // @TODO handle it here… - handleNotification(0, $loadingIndicator, message, icon); // show loading status (cursor) $('body').addClass('loading'); - } + }; /** * hides the loading message @@ -1237,7 +1154,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // hide loading cursor $('body').removeClass('loading'); - } + }; /** * hides any status/error messages @@ -1252,7 +1169,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // also possible: $('.statusmessage').addClass('hidden'); $statusMessage.addClass('hidden'); $errorMessage.addClass('hidden'); - } + }; /** * set a custom handler, which gets all notifications. @@ -1275,7 +1192,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setCustomHandler = function(newHandler) { customHandler = newHandler; - } + }; /** * init status manager @@ -1295,7 +1212,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $loadingIndicator = $('#loadingindicator'); $statusMessage = $('#status'); $remainingTime = $('#remainingtime'); - } + + currentIcon = [ + 'glyphicon-time', // loading icon + 'glyphicon-info-sign', // status icon + '', // reserved for warning, not used yet + 'glyphicon-alert' // error icon + ]; + }; return me; })(); @@ -1304,10 +1228,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * handles paste status/result * * @name PasteStatus - * @param {object} window * @class */ - var PasteStatus = (function (window) { + var PasteStatus = (function () { var me = {}; var $pasteSuccess, @@ -1321,12 +1244,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name PasteStatus.sendToShortener * @private * @function - * @param {Event} event */ - function sendToShortener(event) + function sendToShortener() { - window.location.href = $shortenButton.data('shortener') - + encodeURIComponent($pasteUrl.attr('href')); + window.location.href = $shortenButton.data('shortener') + + encodeURIComponent($pasteUrl.attr('href')); } /** @@ -1337,9 +1259,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name PasteStatus.pasteLinkClick * @function - * @param {Event} event */ - function pasteLinkClick(event) + function pasteLinkClick() { // check if location is (already) shown in URL bar if (window.location.href === $pasteUrl.attr('href')) { @@ -1376,7 +1297,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $pasteSuccess.removeClass('hidden'); // we pre-select the link so that the user only has to [Ctrl]+[c] the link Helper.selectText($pasteUrl[0]); - } + }; /** * shows the remaining time @@ -1408,7 +1329,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ]; Alert.showRemaining([expirationLabel, expiration[0]]); - $remainingTime.removeClass('foryoureyesonly') + $remainingTime.removeClass('foryoureyesonly'); } else { // never expires return; @@ -1416,19 +1337,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // in the end, display notification $remainingTime.removeClass('hidden'); - } + }; /** * hides the remaining time and successful upload notification * - * @name PasteStatus.hideRemainingTime + * @name PasteStatus.hideMessages * @function */ me.hideMessages = function() { $remainingTime.addClass('hidden'); $pasteSuccess.addClass('hidden'); - } + }; /** * init status manager @@ -1440,17 +1361,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $pasteSuccess = $('#pasteSuccess'); + $pasteSuccess = $('#pastesuccess'); // $pasteUrl is saved in me.createPasteNotification() after creation $remainingTime = $('#remainingtime'); $shortenButton = $('#shortenbutton'); // bind elements $shortenButton.click(sendToShortener); - } + }; return me; - })(window); + })(); /** * password prompt @@ -1511,12 +1432,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { throw 'password prompt canceled'; } if (password.length === 0) { - // recursive… + // recurse… return me.requestPassword(); } password = newPassword; - } + }; /** * get the cached password @@ -1531,7 +1452,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getPassword = function() { return password; - } + }; /** * resets the password to an empty string @@ -1570,7 +1491,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }); // handle Model password submission $passwordForm.submit(submitPasswordModal); - } + }; return me; })(); @@ -1666,6 +1587,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show preview PasteViewer.setText($message.val()); + if (AttachmentViewer.hasAttachmentData()) { + var attachmentData = AttachmentViewer.getAttachmentData() || AttachmentViewer.getAttachmentLink().attr('href'); + AttachmentViewer.handleAttachmentPreview(AttachmentViewer.getAttachmentPreview(), attachmentData); + } PasteViewer.run(); // finish @@ -1686,7 +1611,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isPreview = function() { return isPreview; - } + }; /** * reset the Editor view @@ -1703,7 +1628,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // clear content $message.val(''); - } + }; /** * shows the Editor @@ -1715,7 +1640,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $message.removeClass('hidden'); $editorTabs.removeClass('hidden'); - } + }; /** * hides the Editor @@ -1727,7 +1652,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $message.addClass('hidden'); $editorTabs.addClass('hidden'); - } + }; /** * focuses the message input @@ -1738,7 +1663,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.focusInput = function() { $message.focus(); - } + }; /** * sets a new text @@ -1750,7 +1675,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setText = function(newText) { $message.val(newText); - } + }; /** * returns the current text @@ -1761,8 +1686,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.getText = function() { - return $message.val() - } + return $message.val(); + }; /** * init status manager @@ -1784,7 +1709,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // (li) $messageEdit = $('#messageedit').click(viewEditor).parent(); $messagePreview = $('#messagepreview').click(viewPreview).parent(); - } + }; return me; })(); @@ -1822,9 +1747,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - // set text - Helper.setElementText($plainText, text); - Helper.setElementText($prettyPrint, text); + // escape HTML entities, link URLs, sanitize + var escapedLinkedText = Helper.urls2links( + $('
').text(text).html() + ), + sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText); + $plainText.html(sanitizedLinkedText); + $prettyPrint.html(sanitizedLinkedText); switch (format) { case 'markdown': @@ -1833,30 +1762,27 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { tables: true, tablesHeaderId: true }); + // let showdown convert the HTML and sanitize HTML *afterwards*! $plainText.html( - converter.makeHtml(text) + DOMPurify.sanitize(converter.makeHtml(text)) ); // add table classes from bootstrap css $plainText.find('table').addClass('table-condensed table-bordered'); break; case 'syntaxhighlighting': - // @TODO is this really needed or is "one" enough? + // yes, this is really needed to initialize the environment if (typeof prettyPrint === 'function') { prettyPrint(); } $prettyPrint.html( - prettyPrintOne( - Helper.htmlEntities(text), null, true + DOMPurify.sanitize( + prettyPrintOne(escapedLinkedText, null, true) ) ); // fall through, as the rest is the same default: // = 'plaintext' - // convert URLs to clickable links - Helper.urls2links($plainText); - Helper.urls2links($prettyPrint); - $prettyPrint.css('white-space', 'pre-wrap'); $prettyPrint.css('word-break', 'normal'); $prettyPrint.removeClass('prettyprint'); @@ -1874,11 +1800,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { // instead of "nothing" better display a placeholder if (text === '') { - $placeholder.removeClass('hidden') + $placeholder.removeClass('hidden'); return; } // otherwise hide the placeholder - $placeholder.addClass('hidden') + $placeholder.addClass('hidden'); switch (format) { case 'markdown': @@ -1897,7 +1823,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name PasteViewer.setFormat * @function - * @param {string} newFormat the the new format + * @param {string} newFormat the new format */ me.setFormat = function(newFormat) { @@ -1906,14 +1832,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - // needs to update display too, if from or to Markdown is switched + // needs to update display too, if we switch from or to Markdown if (format === 'markdown' || newFormat === 'markdown') { isDisplayed = false; } format = newFormat; isChanged = true; - } + }; /** * returns the current format @@ -1925,7 +1851,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getFormat = function() { return format; - } + }; /** * returns whether the current view is pretty printed @@ -1937,7 +1863,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isPrettyPrinted = function() { return $prettyPrint.hasClass('prettyprinted'); - } + }; /** * sets the text to show @@ -1952,7 +1878,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { text = newText; isChanged = true; } - } + }; /** * gets the current cached text @@ -1964,7 +1890,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getText = function() { return text; - } + }; /** * show/update the parsed text (preview) @@ -1983,7 +1909,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { showPaste(); isDisplayed = true; } - } + }; /** * hide parsed text (preview) @@ -2000,9 +1926,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $plainText.addClass('hidden'); $prettyMessage.addClass('hidden'); $placeholder.addClass('hidden'); + AttachmentViewer.hideAttachmentPreview(); isDisplayed = false; - } + }; /** * init status manager @@ -2035,7 +1962,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // get default option from template/HTML or fall back to set value format = Model.getFormatDefault() || format; - } + text = ''; + isDisplayed = false; + isChanged = true; + }; return me; })(); @@ -2044,17 +1974,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * (view) Show attachment and preview if possible * * @name AttachmentViewer - * @param {object} window - * @param {object} document * @class */ - var AttachmentViewer = (function (window, document) { + var AttachmentViewer = (function () { var me = {}; - var $attachmentLink, - $attachmentPreview, - $attachment; - + var $attachmentLink; + var $attachmentPreview; + var $attachment; + var attachmentData; + var file; + var $fileInput; + var $dragAndDropFileName; var attachmentHasPreview = false; /** @@ -2067,23 +1998,43 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.setAttachment = function(attachmentData, fileName) { - var imagePrefix = 'data:image/'; + // IE does not support setting a data URI on an a element + // Convert dataURI to a Blob and use msSaveBlob to download + if (window.Blob && navigator.msSaveBlob) { + $attachmentLink.off('click').on('click', function () { + // data URI format: data:[][;base64], + + // position in data URI string of where data begins + var base64Start = attachmentData.indexOf(',') + 1; + // position in data URI string of where mediaType ends + var mediaTypeEnd = attachmentData.indexOf(';'); + + // extract mediaType + var mediaType = attachmentData.substring(5, mediaTypeEnd); + // extract data and convert to binary + var decodedData = Base64.atob(attachmentData.substring(base64Start)); + + // Transform into a Blob + var decodedDataLength = decodedData.length; + var buf = new Uint8Array(decodedDataLength); + + for (var i = 0; i < decodedDataLength; i++) { + buf[i] = decodedData.charCodeAt(i); + } + + var blob = new window.Blob([ buf ], { type: mediaType }); + navigator.msSaveBlob(blob, fileName); + }); + } else { + $attachmentLink.attr('href', attachmentData); + } - $attachmentLink.attr('href', attachmentData); if (typeof fileName !== 'undefined') { $attachmentLink.attr('download', fileName); } - // if the attachment is an image, display it - if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) { - $attachmentPreview.html( - $(document.createElement('img')) - .attr('src', attachmentData) - .attr('class', 'img-thumbnail') - ); - attachmentHasPreview = true; - } - } + me.handleAttachmentPreview($attachmentPreview, attachmentData); + }; /** * displays the attachment @@ -2098,12 +2049,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (attachmentHasPreview) { $attachmentPreview.removeClass('hidden'); } - } + }; /** * removes the attachment * - * This automatically hides the attachment containers to, to + * This automatically hides the attachment containers too, to * prevent an inconsistent display. * * @name AttachmentViewer.removeAttachment @@ -2111,12 +2062,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.removeAttachment = function() { + if (!$attachment.length) { + return; + } me.hideAttachment(); me.hideAttachmentPreview(); - $attachmentLink.prop('href', ''); - $attachmentLink.prop('download', ''); + $attachmentLink.removeAttr('href'); + $attachmentLink.removeAttr('download'); + $attachmentLink.off('click'); $attachmentPreview.html(''); - } + + file = undefined; + attachmentData = undefined; + }; /** * hides the attachment @@ -2131,7 +2089,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideAttachment = function() { $attachment.addClass('hidden'); - } + }; /** * hides the attachment preview @@ -2141,8 +2099,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.hideAttachmentPreview = function() { - $attachmentPreview.addClass('hidden'); - } + if ($attachmentPreview) { + $attachmentPreview.addClass('hidden'); + } + }; /** * checks if there is an attachment @@ -2152,9 +2112,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.hasAttachment = function() { + if (!$attachment.length) { + return false; + } var link = $attachmentLink.prop('href'); - return (typeof link !== 'undefined' && link !== '') - } + return (typeof link !== 'undefined' && link !== ''); + }; + + /** + * checks if there is attachment data available + * + * @name AttachmentViewer.hasAttachmentData + * @function + */ + me.hasAttachmentData = function() + { + if ($attachment.length) { + return true; + } + return false; + }; /** * return the attachment @@ -2169,7 +2146,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attachmentLink.prop('href'), $attachmentLink.prop('download') ]; - } + }; /** * moves the attachment link to another element @@ -2188,7 +2165,225 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // update text I18n._($attachmentLink, label, $attachmentLink.attr('download')); - } + }; + + /** + * read file data as dataURL using the FileReader API + * + * @name AttachmentViewer.readFileData + * @function + * @param {object} loadedFile The loaded file. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL()} + */ + me.readFileData = function (loadedFile) { + if (typeof FileReader === 'undefined') { + // revert loading status… + me.hideAttachment(); + me.hideAttachmentPreview(); + Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.'); + return; + } + + var fileReader = new FileReader(); + if (loadedFile === undefined) { + loadedFile = $fileInput[0].files[0]; + $dragAndDropFileName.text(''); + } else { + $dragAndDropFileName.text(loadedFile.name); + } + + file = loadedFile; + + fileReader.onload = function (event) { + var dataURL = event.target.result; + attachmentData = dataURL; + + if (Editor.isPreview()) { + me.handleAttachmentPreview($attachmentPreview, dataURL); + $attachmentPreview.removeClass('hidden'); + } + }; + fileReader.readAsDataURL(loadedFile); + }; + + /** + * handle the preview of files that can either be an image, video, audio or pdf element + * + * @name AttachmentViewer.handleAttachmentPreview + * @function + * @argument {jQuery} $targetElement where the preview should be appended. + * @argument {File Data} data of the file to be displayed. + */ + me.handleAttachmentPreview = function ($targetElement, data) { + if (data) { + // source: https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL() + var mimeType = data.slice( + data.indexOf('data:') + 5, + data.indexOf(';base64,') + ); + + attachmentHasPreview = true; + if (mimeType.match(/image\//i)) { + $targetElement.html( + $(document.createElement('img')) + .attr('src', data) + .attr('class', 'img-thumbnail') + ); + } else if (mimeType.match(/video\//i)) { + $targetElement.html( + $(document.createElement('video')) + .attr('controls', 'true') + .attr('autoplay', 'true') + .attr('class', 'img-thumbnail') + + .append($(document.createElement('source')) + .attr('type', mimeType) + .attr('src', data)) + ); + } else if (mimeType.match(/audio\//i)) { + $targetElement.html( + $(document.createElement('audio')) + .attr('controls', 'true') + .attr('autoplay', 'true') + + .append($(document.createElement('source')) + .attr('type', mimeType) + .attr('src', data)) + ); + } else if (mimeType.match(/\/pdf/i)) { + // PDFs are only displayed if the filesize is smaller than about 1MB (after base64 encoding). + // Bigger filesizes currently cause crashes in various browsers. + // See also: https://code.google.com/p/chromium/issues/detail?id=69227 + + // Firefox crashes with files that are about 1.5MB + // The performance with 1MB files is bearable + if (data.length > 1398488) { + Alert.showError('File too large, to display a preview. Please download the attachment.'); + return; + } + + // Fallback for browsers, that don't support the vh unit + var clientHeight = $(window).height(); + + $targetElement.html( + $(document.createElement('embed')) + .attr('src', data) + .attr('type', 'application/pdf') + .attr('class', 'pdfPreview') + .css('height', clientHeight) + ); + } else { + attachmentHasPreview = false; + } + } + }; + + /** + * attaches the file attachment drag & drop handler to the page + * + * @name AttachmentViewer.addDragDropHandler + * @function + */ + me.addDragDropHandler = function () { + if (typeof $fileInput === 'undefined' || $fileInput.length === 0) { + return; + } + + var ignoreDragDrop = function(event) { + event.stopPropagation(); + event.preventDefault(); + }; + + var drop = function(event) { + var evt = event.originalEvent; + evt.stopPropagation(); + evt.preventDefault(); + + if ($fileInput) { + var file = evt.dataTransfer.files[0]; + //Clear the file input: + $fileInput.wrap('
').closest('form').get(0).reset(); + $fileInput.unwrap(); + //Only works in Chrome: + //fileInput[0].files = e.dataTransfer.files; + + me.readFileData(file); + } + }; + + $(document).on('drop', drop); + $(document).on('dragenter', ignoreDragDrop); + $(document).on('dragover', ignoreDragDrop); + $fileInput.on("change", function () { + me.readFileData(); + }); + }; + + /** + * attaches the clipboard attachment handler to the page + * + * @name AttachmentViewer.addClipboardEventHandler + * @function + */ + me.addClipboardEventHandler = function () { + $(document).on('paste', + function (event) { + var items = (event.clipboardData || event.originalEvent.clipboardData).items; + for (var i in items) { + if (items.hasOwnProperty(i)) { + var item = items[i]; + if (item.kind === 'file') { + me.readFileData(item.getAsFile()); + } + } + } + }); + }; + + + /** + * getter for attachment data + * + * @name AttachmentViewer.getAttachmentData + * @function + * @return {jQuery} + */ + me.getAttachmentData = function () { + return attachmentData; + }; + + /** + * getter for attachment link + * + * @name AttachmentViewer.getAttachmentLink + * @function + * @return {jQuery} + */ + me.getAttachmentLink = function () { + return $attachmentLink; + }; + + /** + * getter for attachment preview + * + * @name AttachmentViewer.getAttachmentPreview + * @function + * @return {jQuery} + */ + me.getAttachmentPreview = function () { + return $attachmentPreview; + }; + + /** + * getter for file data, returns the file contents + * + * @name AttachmentViewer.getFile + * @function + * @return {string} + */ + me.getFile = function () { + return file; + }; /** * initiate @@ -2201,22 +2396,27 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { $attachment = $('#attachment'); - $attachmentLink = $('#attachment a'); - $attachmentPreview = $('#attachmentPreview'); + if($attachment.length){ + $attachmentLink = $('#attachment a'); + $attachmentPreview = $('#attachmentPreview'); + $dragAndDropFileName = $('#dragAndDropFileName'); + + $fileInput = $('#file'); + me.addDragDropHandler(); + me.addClipboardEventHandler(); + } } return me; - })(window, document); + })(); /** * (view) Shows discussion thread and handles replies * * @name DiscussionViewer - * @param {object} window - * @param {object} document * @class */ - var DiscussionViewer = (function (window, document) { + var DiscussionViewer = (function () { var me = {}; var $commentTail, @@ -2282,12 +2482,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name DiscussionViewer.handleNotification * @function * @param {string} alertType - * @param {jQuery} $element - * @param {string|array} args - * @param {string|null} icon * @return {bool|jQuery} */ - me.handleNotification = function(alertType, $element, args, icon) + me.handleNotification = function(alertType) { // ignore loading messages if (alertType === 'loading') { @@ -2307,7 +2504,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return $replyStatus; - } + }; /** * adds another comment @@ -2316,22 +2513,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {object} comment * @param {string} commentText - * @param {jQuery} $place - optional, tries to find the best position otherwise + * @param {string} nickname */ - me.addComment = function(comment, commentText, nickname, $place) + me.addComment = function(comment, commentText, nickname) { - if (typeof $place === 'undefined') { - // starting point (default value/fallback) - $place = $commentContainer; - - // if parent comment exists - var $parentComment = $('#comment_' + comment.parentid); - if ($parentComment.length) { - // use parent as position for noew comment, so it shifted - // to the right - $place = $parentComment; - } - } if (commentText === '') { commentText = 'comment decryption failed'; } @@ -2342,8 +2527,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var $commentEntryData = $commentEntry.find('div.commentdata'); // set & parse text - Helper.setElementText($commentEntryData, commentText); - Helper.urls2links($commentEntryData); + $commentEntryData.html( + DOMPurify.sanitize( + Helper.urls2links(commentText) + ) + ); // set nickname if (nickname.length > 0) { @@ -2370,9 +2558,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }); } + // starting point (default value/fallback) + var $place = $commentContainer; + + // if parent comment exists + var $parentComment = $('#comment_' + comment.parentid); + if ($parentComment.length) { + // use parent as position for new comment, so it is shifted + // to the right + $place = $parentComment; + } + // finally append comment $place.append($commentEntry); - } + }; /** * finishes the discussion area after last comment @@ -2387,49 +2586,59 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show discussions $discussion.removeClass('hidden'); - } - - /** - * shows the discussion area - * - * @name DiscussionViewer.showDiscussion - * @function - */ - me.showDiscussion = function() - { - $discussion.removeClass('hidden'); - } + }; /** * removes the old discussion and prepares everything for creating a new * one. * - * @name DiscussionViewer.prepareNewDisucssion + * @name DiscussionViewer.prepareNewDiscussion * @function */ - me.prepareNewDisucssion = function() + me.prepareNewDiscussion = function() { $commentContainer.html(''); $discussion.addClass('hidden'); // (re-)init templates initTemplates(); - } + }; /** - * returns the user put into the reply form + * returns the users message from the reply form * - * @name DiscussionViewer.getReplyData + * @name DiscussionViewer.getReplyMessage * @function - * @return {array} + * @return {String} */ - me.getReplyData = function() + me.getReplyMessage = function() { - return [ - $replyMessage.val(), - $replyNickname.val() - ]; - } + return $replyMessage.val(); + }; + + /** + * returns the users nickname (if any) from the reply form + * + * @name DiscussionViewer.getReplyNickname + * @function + * @return {String} + */ + me.getReplyNickname = function() + { + return $replyNickname.val(); + }; + + /** + * returns the id of the parent comment the user is replying to + * + * @name DiscussionViewer.getReplyCommentId + * @function + * @return {int|undefined} + */ + me.getReplyCommentId = function() + { + return replyCommentId; + }; /** * highlights a specific comment and scrolls to it if necessary @@ -2454,26 +2663,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $comment.removeClass('highlight'); }, 300); } - } + }; if (UiHelper.isVisible($comment)) { return highlightComment(); } UiHelper.scrollTo($comment, 100, 'swing', highlightComment); - } - - /** - * returns the id of the parent comment the user is replying to - * - * @name DiscussionViewer.getReplyCommentId - * @function - * @return {int|undefined} - */ - me.getReplyCommentId = function() - { - return replyCommentId; - } + }; /** * initiate @@ -2491,10 +2688,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $commentContainer = $('#commentcontainer'); $discussion = $('#discussion'); - } + }; return me; - })(window, document); + })(); /** * Manage top (navigation) bar @@ -2525,6 +2722,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $password, $passwordInput, $rawTextButton, + $qrCodeLink, $sendButton, $retryButton; @@ -2623,12 +2821,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name TopNav.rawText * @private * @function - * @param {Event} event */ - function rawText(event) + function rawText() { TopNav.hideAllButtons(); - Alert.showLoading('Showing raw text…', 0, 'time'); + Alert.showLoading('Showing raw text…', 'time'); var paste = PasteViewer.getText(); // push a new state to allow back navigation with browser back button @@ -2648,7 +2845,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { for (var i = 0; i < $head.length; i++) { newDoc.write($head[i].outerHTML); } - newDoc.write('
' + Helper.htmlEntities(paste) + '
'); + newDoc.write('
' + DOMPurify.sanitize(paste) + '
'); newDoc.close(); } @@ -2672,9 +2869,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name TopNav.clickNewPaste * @private * @function - * @param {Event} event */ - function clickNewPaste(event) + function clickNewPaste() { Controller.hideStatusMessages(); Controller.newPaste(); @@ -2718,7 +2914,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * Shows all elements belonging to viwing an existing pastes + * Shows the QR code of the current paste (URL). + * + * @name TopNav.displayQrCode + * @private + * @function + */ + function displayQrCode() + { + var qrCanvas = kjua({ + render: 'canvas', + text: window.location.href + }); + $('#qrcode-display').html(qrCanvas); + } + + /** + * Shows all navigation elements for viewing an existing paste * * @name TopNav.showViewButtons * @function @@ -2726,19 +2938,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showViewButtons = function() { if (viewButtonsDisplayed) { - console.log('showViewButtons: view buttons are already displayed'); + console.warn('showViewButtons: view buttons are already displayed'); return; } $newButton.removeClass('hidden'); $cloneButton.removeClass('hidden'); $rawTextButton.removeClass('hidden'); + $qrCodeLink.removeClass('hidden'); viewButtonsDisplayed = true; - } + }; /** - * Hides all elements belonging to existing pastes + * Hides all navigation elements for viewing an existing paste * * @name TopNav.hideViewButtons * @function @@ -2746,16 +2959,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideViewButtons = function() { if (!viewButtonsDisplayed) { - console.log('hideViewButtons: view buttons are already hidden'); + console.warn('hideViewButtons: view buttons are already hidden'); return; } $cloneButton.addClass('hidden'); $newButton.addClass('hidden'); $rawTextButton.addClass('hidden'); + $qrCodeLink.addClass('hidden'); viewButtonsDisplayed = false; - } + }; /** * Hides all elements belonging to existing pastes @@ -2767,7 +2981,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { me.hideViewButtons(); me.hideCreateButtons(); - } + }; /** * shows all elements needed when creating a new paste @@ -2778,7 +2992,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showCreateButtons = function() { if (createButtonsDisplayed) { - console.log('showCreateButtons: create buttons are already displayed'); + console.warn('showCreateButtons: create buttons are already displayed'); return; } @@ -2792,7 +3006,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $sendButton.removeClass('hidden'); createButtonsDisplayed = true; - } + }; /** * shows all elements needed when creating a new paste @@ -2803,7 +3017,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideCreateButtons = function() { if (!createButtonsDisplayed) { - console.log('hideCreateButtons: create buttons are already hidden'); + console.warn('hideCreateButtons: create buttons are already hidden'); return; } @@ -2817,7 +3031,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attach.addClass('hidden'); createButtonsDisplayed = false; - } + }; /** * only shows the "new paste" button @@ -2828,7 +3042,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showNewPasteButton = function() { $newButton.removeClass('hidden'); - } + }; /** * only shows the "retry" button @@ -2861,7 +3075,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideCloneButton = function() { $cloneButton.addClass('hidden'); - } + }; /** * only hides the raw text button @@ -2872,7 +3086,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideRawButton = function() { $rawTextButton.addClass('hidden'); - } + }; /** * hides the file selector in attachment @@ -2883,7 +3097,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideFileSelector = function() { $fileWrap.addClass('hidden'); - } + }; /** @@ -2895,24 +3109,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showCustomAttachment = function() { $customAttachment.removeClass('hidden'); - } + }; /** - * collapses the navigation bar if nedded + * collapses the navigation bar, only if expanded * * @name TopNav.collapseBar * @function */ me.collapseBar = function() { - var $bar = $('.navbar-toggle'); - - // check if bar is expanded - if ($bar.hasClass('collapse in')) { - // if so, toggle it - $bar.click(); + if ($('#navbar').attr('aria-expanded') === 'true') { + $('.navbar-toggle').click(); } - } + }; /** * returns the currently set expiration time @@ -2924,7 +3134,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getExpiration = function() { return pasteExpiration; - } + }; /** * returns the currently selected file(s) @@ -2941,13 +3151,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (!$file.length || !$file[0].files.length) { return null; } - // @TODO is this really necessary + + // ensure the selected file is still accessible if (!($file[0].files && $file[0].files[0])) { return null; } return $file[0].files; - } + }; /** * returns the state of the burn after reading checkbox @@ -2959,7 +3170,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getBurnAfterReading = function() { return $burnAfterReading.is(':checked'); - } + }; /** * returns the state of the discussion checkbox @@ -2971,7 +3182,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getOpenDiscussion = function() { return $openDiscussion.is(':checked'); - } + }; /** * returns the entered password @@ -2983,7 +3194,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getPassword = function() { return $passwordInput.val(); - } + }; /** * returns the element where custom attachments can be placed @@ -2997,7 +3208,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getCustomAttachment = function() { return $customAttachment; - } + }; /** * Set a function to call when the retry button is clicked. @@ -3038,6 +3249,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $rawTextButton = $('#rawtextbutton'); $retryButton = $('#retrybutton'); $sendButton = $('#sendbutton'); + $qrCodeLink = $('#qrcodelink'); // bootstrap template drop down $('#language ul.dropdown-menu li a').click(setLanguage); @@ -3053,6 +3265,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $rawTextButton.click(rawText); $retryButton.click(clickRetryButton); $fileRemoveButton.click(removeAttachment); + $qrCodeLink.click(displayQrCode); // bootstrap template drop downs $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration); @@ -3064,7 +3277,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // get default value from template or fall back to set value pasteExpiration = Model.getExpirationDefault() || pasteExpiration; - } + + createButtonsDisplayed = false; + viewButtonsDisplayed = false; + }; return me; })(window, document); @@ -3206,7 +3422,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { console.error(textStatus, errorThrown); fail(3, jqXHR); }); - } + }; /** * set success function @@ -3218,7 +3434,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUrl = function(newUrl) { url = newUrl; - } + }; /** * sets the password to use (first value) and optionally also the @@ -3238,7 +3454,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (typeof newKey !== 'undefined') { symmetricKey = newKey; } - } + }; /** * set success function @@ -3250,7 +3466,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setSuccess = function(func) { successFunc = func; - } + }; /** * set failure function @@ -3262,7 +3478,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setFailure = function(func) { failureFunc = func; - } + }; /** * prepares a new upload @@ -3290,7 +3506,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { failureFunc = null; url = Helper.baseUri(); data = {}; - } + }; /** * encrypts and sets the data @@ -3304,7 +3520,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { checkCryptParameters(); data[index] = CryptTool.cipher(symmetricKey, password, element); - } + }; /** * set the additional metadata to send unencrypted @@ -3317,7 +3533,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUnencryptedData = function(index, element) { data[index] = element; - } + }; /** * set the additional metadata to send unencrypted passed at once @@ -3329,7 +3545,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUnencryptedBulkData = function(newData) { $.extend(data, newData); - } + }; /** * Helper, which parses shows a general error message based on the result of the Uploader @@ -3345,13 +3561,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var errorArray; switch (status) { - case me.error['custom']: + case me.error.custom: errorArray = ['Could not ' + doThisThing + ': %s', data.message]; break; - case me.error['unknown']: + case me.error.unknown: errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')]; break; - case me.error['serverError']: + case me.error.serverError: errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')]; break; default: @@ -3360,7 +3576,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return errorArray; - } + }; /** * init Uploader @@ -3371,7 +3587,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - } + }; return me; })(); @@ -3435,7 +3651,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.hideMessages(); // show notification - PasteStatus.createPasteNotification(url, deleteUrl) + PasteStatus.createPasteNotification(url, deleteUrl); // show new URL in browser bar history.pushState({type: 'newpaste'}, document.title, url); @@ -3460,7 +3676,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ function showUploadedComment(status, data) { // show success message - // Alert.showStatus('Comment posted.'); + Alert.showStatus('Comment posted.'); // reload paste Controller.refreshPaste(function () { @@ -3477,31 +3693,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name PasteEncrypter.encryptAttachments * @private * @function - * @param {File|null|undefined} file - optional, falls back to cloned attachment * @param {function} callback - excuted when action is successful */ - function encryptAttachments(file, callback) { + function encryptAttachments(callback) { + var file = AttachmentViewer.getAttachmentData(); + if (typeof file !== 'undefined' && file !== null) { - // check file reader requirements for upload - if (typeof FileReader === 'undefined') { - Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.'); - // cancels process as it does not execute callback - return; - } + var fileName = AttachmentViewer.getFile().name; - var reader = new FileReader(); + Uploader.setData('attachment', file); + Uploader.setData('attachmentname', fileName); - // closure to capture the file information - reader.onload = function(event) { - Uploader.setData('attachment', event.target.result); - Uploader.setData('attachmentname', file.name); - - // run callback - return callback(); - } - - // actually read first file - reader.readAsDataURL(file); + // run callback + return callback(); } else if (AttachmentViewer.hasAttachment()) { // fall back to cloned part var attachment = AttachmentViewer.getAttachment(); @@ -3528,12 +3732,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // UI loading state TopNav.hideAllButtons(); - Alert.showLoading('Sending comment…', 0, 'cloud-upload'); + Alert.showLoading('Sending comment…', 'cloud-upload'); - // get data, note that "var [x, y] = " structures aren't supported in all JS environments - var replyData = DiscussionViewer.getReplyData(), - plainText = replyData[0], - nickname = replyData[1], + // get data + var plainText = DiscussionViewer.getReplyMessage(), + nickname = DiscussionViewer.getReplyNickname(), parentid = DiscussionViewer.getReplyCommentId(); // do not send if there is no data @@ -3551,7 +3754,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })) { return; // to prevent multiple executions } - Alert.showLoading(null, 10); // prepare Uploader Uploader.prepare(); @@ -3565,7 +3767,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { TopNav.showViewButtons(); // show error message - Alert.showError(Uploader.parseUploadError(status, data, 'post comment')); + Alert.showError( + Uploader.parseUploadError(status, data, 'post comment') + ); // reset error handler Alert.setCustomHandler(null); @@ -3575,7 +3779,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.setUnencryptedData('pasteid', Model.getPasteId()); if (typeof parentid === 'undefined') { // if parent id is not set, this is the top-most comment, so use - // paste id as parent @TODO is this really good? + // paste id as parent, as the root element of the discussion tree Uploader.setUnencryptedData('parentid', Model.getPasteId()); } else { Uploader.setUnencryptedData('parentid', parentid); @@ -3589,7 +3793,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } Uploader.run(); - } + }; /** * sends a new paste to server @@ -3604,13 +3808,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // UI loading state TopNav.hideAllButtons(); - Alert.showLoading('Sending paste…', 0, 'cloud-upload'); + Alert.showLoading('Sending paste…', 'cloud-upload'); TopNav.collapseBar(); // get data var plainText = Editor.getText(), format = PasteViewer.getFormat(), - files = TopNav.getFileList(); + files = TopNav.getFileList() || AttachmentViewer.getFile() || AttachmentViewer.hasAttachment(); // do not send if there is no data if (plainText.length === 0 && files === null) { @@ -3620,8 +3824,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - Alert.showLoading(null, 10); - // check entropy if (!checkRequirements(function () { me.sendPaste(); @@ -3641,7 +3843,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { TopNav.showCreateButtons(); // show error message - Alert.showError(Uploader.parseUploadError(status, data, 'create paste')); + Alert.showError( + Uploader.parseUploadError(status, data, 'create paste') + ); }); // fill it with unencrypted submitted options @@ -3661,13 +3865,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // encrypt attachments encryptAttachments( - files === null ? null : files[0], function () { // send data Uploader.run(); } ); - } + }; /** * initialize @@ -3678,7 +3881,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - } + }; return me; })(); @@ -3762,7 +3965,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { try { plaintext = decryptOrPromptPassword(key, password, paste.data); } catch (err) { - throw 'failed to decipher paste text: ' + err + throw 'failed to decipher paste text: ' + err; } if (plaintext === false) { return false; @@ -3792,23 +3995,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ function decryptAttachment(paste, key, password) { + var attachment, attachmentName; + // decrypt attachment try { - var attachment = decryptOrPromptPassword(key, password, paste.attachment); + attachment = decryptOrPromptPassword(key, password, paste.attachment); } catch (err) { - throw 'failed to decipher attachment: ' + err + throw 'failed to decipher attachment: ' + err; } if (attachment === false) { return false; } // decrypt attachment name - var attachmentName; if (paste.attachmentname) { try { attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname); } catch (err) { - throw 'failed to decipher attachment name: ' + err + throw 'failed to decipher attachment name: ' + err; } if (attachmentName === false) { return false; @@ -3835,7 +4039,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { function decryptComments(paste, key, password) { // remove potentially previous discussion - DiscussionViewer.prepareNewDisucssion(); + DiscussionViewer.prepareNewDiscussion(); // iterate over comments for (var i = 0; i < paste.comments.length; ++i) { @@ -3849,7 +4053,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } DiscussionViewer.finishDiscussion(); - DiscussionViewer.showDiscussion(); return true; } @@ -3863,7 +4066,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.run = function(paste) { Alert.hideMessages(); - Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons + Alert.showLoading('Decrypting paste…', 'cloud-download'); if (typeof paste === 'undefined') { // get cipher data and wait until it is available @@ -3875,7 +4078,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { password = Prompt.getPassword(); if (PasteViewer.isPrettyPrinted()) { - console.error('Too pretty! (don\'t know why this check)'); //@TODO + // don't decrypt twice return; } @@ -3883,10 +4086,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { try { // decrypt attachments if (paste.attachment) { - // try to decrypt paste and if it fails (because the password is - // missing) return to let JS continue and wait for user - if (!decryptAttachment(paste, key, password)) { - return; + if (AttachmentViewer.hasAttachmentData()) { + // try to decrypt paste and if it fails (because the password is + // missing) return to let JS continue and wait for user + if (!decryptAttachment(paste, key, password)) { + return; + } } // ignore empty paste, as this is allowed when pasting attachments decryptPaste(paste, key, password, true); @@ -3921,7 +4126,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }); TopNav.showRetryButton(); } - } + }; /** * initialize @@ -3932,7 +4137,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - } + }; return me; })(); @@ -3958,7 +4163,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { PasteStatus.hideMessages(); Alert.hideMessages(); - } + }; /** * creates a new paste @@ -3971,17 +4176,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // Important: This *must not* run Alert.hideMessages() as previous // errors from viewing a paste should be shown. TopNav.hideAllButtons(); - Alert.showLoading('Preparing new paste…', 0, 'time'); + Alert.showLoading('Preparing new paste…', 'time'); PasteStatus.hideMessages(); PasteViewer.hide(); Editor.resetInput(); Editor.show(); Editor.focusInput(); + AttachmentViewer.removeAttachment(); TopNav.showCreateButtons(); Alert.hideLoading(); - } + }; /** * shows how we much we love bots that execute JS ;) @@ -4011,14 +4217,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // missing decryption key (or paste ID) in URL? if (window.location.hash.length === 0) { Alert.showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)'); - // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error return; } } // show proper elements on screen PasteDecrypter.run(); - } + }; /** * refreshes the loaded paste to show potential new data @@ -4033,15 +4238,33 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var orgPosition = $(window).scrollTop(); Model.getPasteData(function (data) { - // restore position - window.scrollTo(0, orgPosition); + Uploader.prepare(); + Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId()); - PasteDecrypter.run(data); + Uploader.setFailure(function (status, data) { + // revert loading status… + Alert.hideLoading(); + TopNav.showViewButtons(); - // NOTE: could create problems as callback may be called - // asyncronously if PasteDecrypter e.g. needs to wait for a - // password being entered - callback(); + // show error message + Alert.showError( + Uploader.parseUploadError(status, data, 'refresh display') + ); + }); + Uploader.setSuccess(function (status, data) { + PasteDecrypter.run(data); + + // restore position + window.scrollTo(0, orgPosition); + + PasteDecrypter.run(data); + + // NOTE: could create problems as callback may be called + // asyncronously if PasteDecrypter e.g. needs to wait for a + // password being entered + callback(); + }); + Uploader.run(); }, false); // this false is important as it circumvents the cache } @@ -4050,13 +4273,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name Controller.clonePaste * @function - * @param {Event} event */ - me.clonePaste = function(event) + me.clonePaste = function() { TopNav.collapseBar(); TopNav.hideAllButtons(); - Alert.showLoading('Cloning paste…', 0, 'transfer'); + Alert.showLoading('Cloning paste…', 'transfer'); // hide messages from previous paste me.hideStatusMessages(); @@ -4082,16 +4304,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { [ 'The cloned file \'%s\' was attached to this paste.', AttachmentViewer.getAttachment()[1] - ], 'copy', true, true); + ], + 'copy' + ); } - Editor.setText(PasteViewer.getText()) + Editor.setText(PasteViewer.getText()); PasteViewer.hide(); Editor.show(); Alert.hideLoading(); TopNav.showCreateButtons(); - } + }; /** * removes a saved paste @@ -4110,10 +4334,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.setUnencryptedData('deletetoken', deleteToken); Uploader.setFailure(function () { - Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); - }) + Alert.showError( + I18n._('Could not delete the paste, it was not stored in burn after reading mode.') + ); + }); Uploader.run(); - } + }; /** * application start @@ -4126,10 +4352,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // first load translations I18n.loadTranslations(); + DOMPurify.setConfig({SAFE_FOR_JQUERY: true}); + // initialize other modules/"classes" Alert.init(); Model.init(); - AttachmentViewer.init(); DiscussionViewer.init(); Editor.init(); @@ -4157,7 +4384,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return me.showBadBotMessage(); } - //display an existing paste + // display an existing paste return me.showPaste(); } @@ -4183,4 +4410,4 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { PasteDecrypter: PasteDecrypter, Controller: Controller }; -}(jQuery, sjcl, Base64, RawDeflate); +})(jQuery, sjcl, Base64, RawDeflate); diff --git a/js/purify-1.0.3.js b/js/purify-1.0.3.js new file mode 100644 index 0000000..b5368e3 --- /dev/null +++ b/js/purify-1.0.3.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.DOMPurify=t()}(this,function(){"use strict";function e(e,t){for(var n=t.length;n--;)"string"==typeof t[n]&&(t[n]=t[n].toLowerCase()),e[t[n]]=!0;return e}function t(e){var t={},n=void 0;for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t}function n(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:A(),S=function(e){return o(e)};if(S.version="1.1.1",S.removed=[],!x||!x.document||9!==x.document.nodeType)return S.isSupported=!1,S;var k=x.document,w=!1,E=!1,O=x.document,L=x.DocumentFragment,M=x.HTMLTemplateElement,N=x.Node,_=x.NodeFilter,D=x.NamedNodeMap,R=void 0===D?x.NamedNodeMap||x.MozNamedAttrMap:D,C=x.Text,F=x.Comment,z=x.DOMParser,H=x.XMLHttpRequest,I=void 0===H?x.XMLHttpRequest:H,j=x.encodeURI,U=void 0===j?x.encodeURI:j;if("function"==typeof M){var W=O.createElement("template");W.content&&W.content.ownerDocument&&(O=W.content.ownerDocument)}var q=O,G=q.implementation,P=q.createNodeIterator,B=q.getElementsByTagName,X=q.createDocumentFragment,V=k.importNode,Y={};S.isSupported=G&&void 0!==G.createHTMLDocument&&9!==O.documentMode;var K=p,$=f,J=h,Q=g,Z=v,ee=b,te=y,ne=null,oe=e({},[].concat(n(r),n(i),n(a),n(l),n(s))),re=null,ie=e({},[].concat(n(c),n(d),n(u),n(m))),ae=null,le=null,se=!0,ce=!0,de=!1,ue=!1,me=!1,pe=!1,fe=!1,he=!1,ge=!1,ye=!1,ve=!1,be=!0,Te=!0,Ae={},xe=e({},["audio","head","math","script","style","template","svg","video"]),Se=e({},["audio","video","img","source","image"]),ke=e({},["alt","class","for","id","label","name","pattern","placeholder","summary","title","value","style","xmlns"]),we=null,Ee=O.createElement("form"),Oe=function(o){"object"!==(void 0===o?"undefined":T(o))&&(o={}),ne="ALLOWED_TAGS"in o?e({},o.ALLOWED_TAGS):oe,re="ALLOWED_ATTR"in o?e({},o.ALLOWED_ATTR):ie,ae="FORBID_TAGS"in o?e({},o.FORBID_TAGS):{},le="FORBID_ATTR"in o?e({},o.FORBID_ATTR):{},Ae="USE_PROFILES"in o&&o.USE_PROFILES,se=!1!==o.ALLOW_ARIA_ATTR,ce=!1!==o.ALLOW_DATA_ATTR,de=o.ALLOW_UNKNOWN_PROTOCOLS||!1,ue=o.SAFE_FOR_JQUERY||!1,me=o.SAFE_FOR_TEMPLATES||!1,pe=o.WHOLE_DOCUMENT||!1,ge=o.RETURN_DOM||!1,ye=o.RETURN_DOM_FRAGMENT||!1,ve=o.RETURN_DOM_IMPORT||!1,he=o.FORCE_BODY||!1,be=!1!==o.SANITIZE_DOM,Te=!1!==o.KEEP_CONTENT,te=o.ALLOWED_URI_REGEXP||te,me&&(ce=!1),ye&&(ge=!0),Ae&&(ne=e({},[].concat(n(s))),re=[],!0===Ae.html&&(e(ne,r),e(re,c)),!0===Ae.svg&&(e(ne,i),e(re,d),e(re,m)),!0===Ae.svgFilters&&(e(ne,a),e(re,d),e(re,m)),!0===Ae.mathMl&&(e(ne,l),e(re,u),e(re,m))),o.ADD_TAGS&&(ne===oe&&(ne=t(ne)),e(ne,o.ADD_TAGS)),o.ADD_ATTR&&(re===ie&&(re=t(re)),e(re,o.ADD_ATTR)),o.ADD_URI_SAFE_ATTR&&e(ke,o.ADD_URI_SAFE_ATTR),Te&&(ne["#text"]=!0),Object&&"freeze"in Object&&Object.freeze(o),we=o},Le=function(e){S.removed.push({element:e});try{e.parentNode.removeChild(e)}catch(t){e.outerHTML=""}},Me=function(e,t){S.removed.push({attribute:t.getAttributeNode(e),from:t}),t.removeAttribute(e)},Ne=function(e){var t=void 0,n=void 0;if(he&&(e=""+e),E){try{e=U(e)}catch(e){}var o=new I;o.responseType="document",o.open("GET","data:text/html;charset=utf-8,"+e,!1),o.send(null),t=o.response}if(w)try{t=(new z).parseFromString(e,"text/html")}catch(e){}return t&&t.documentElement||((n=(t=G.createHTMLDocument("")).body).parentNode.removeChild(n.parentNode.firstElementChild),n.outerHTML=e),B.call(t,pe?"html":"body")[0]};S.isSupported&&function(){var e=Ne('');e.querySelector("svg")||(E=!0);try{(e=Ne('

')).querySelector("svg img")&&(w=!0)}catch(e){}}();var _e=function(e){return P.call(e.ownerDocument||e,e,_.SHOW_ELEMENT|_.SHOW_COMMENT|_.SHOW_TEXT,function(){return _.FILTER_ACCEPT},!1)},De=function(e){return!(e instanceof C||e instanceof F)&&!("string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof R&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute)},Re=function(e){return"object"===(void 0===N?"undefined":T(N))?e instanceof N:e&&"object"===(void 0===e?"undefined":T(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},Ce=function(e,t,n){Y[e]&&Y[e].forEach(function(e){e.call(S,t,n,we)})},Fe=function(e){var t=void 0;if(Ce("beforeSanitizeElements",e,null),De(e))return Le(e),!0;var n=e.nodeName.toLowerCase();if(Ce("uponSanitizeElement",e,{tagName:n,allowedTags:ne}),!ne[n]||ae[n]){if(Te&&!xe[n]&&"function"==typeof e.insertAdjacentHTML)try{e.insertAdjacentHTML("AfterEnd",e.innerHTML)}catch(e){}return Le(e),!0}return!ue||e.firstElementChild||e.content&&e.content.firstElementChild||!/l&&e.setAttribute("id",i.value);else{if("INPUT"===e.nodeName&&"type"===r&&"file"===o&&(re[r]||!le[r]))continue;"id"===n&&e.setAttribute(n,""),Me(n,e)}if(s.keepAttr&&(!be||"id"!==r&&"name"!==r||!(o in O||o in Ee))){if(me&&(o=(o=o.replace(K," ")).replace($," ")),ce&&J.test(r));else if(se&&Q.test(r));else{if(!re[r]||le[r])continue;if(ke[r]);else if(te.test(o.replace(ee,"")));else if("src"!==r&&"xlink:href"!==r||0!==o.indexOf("data:")||!Se[e.nodeName.toLowerCase()]){if(de&&!Z.test(o.replace(ee,"")));else if(o)continue}else;}try{e.setAttribute(n,o),S.removed.pop()}catch(e){}}}Ce("afterSanitizeAttributes",e,null)}},He=function e(t){var n=void 0,o=_e(t);for(Ce("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)Ce("uponSanitizeShadowNode",n,null),Fe(n)||(n.content instanceof L&&e(n.content),ze(n));Ce("afterSanitizeShadowDOM",t,null)};return S.sanitize=function(e,t){var n=void 0,o=void 0,r=void 0,i=void 0,a=void 0;if(e||(e="\x3c!--\x3e"),"string"!=typeof e&&!Re(e)){if("function"!=typeof e.toString)throw new TypeError("toString is not a function");if("string"!=typeof(e=e.toString()))throw new TypeError("dirty is not a string, aborting")}if(!S.isSupported){if("object"===T(x.toStaticHTML)||"function"==typeof x.toStaticHTML){if("string"==typeof e)return x.toStaticHTML(e);if(Re(e))return x.toStaticHTML(e.outerHTML)}return e}if(fe||Oe(t),S.removed=[],e instanceof N)1===(o=(n=Ne("\x3c!--\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===o.nodeName?n=o:n.appendChild(o);else{if(!ge&&!pe&&-1===e.indexOf("<"))return e;if(!(n=Ne(e)))return ge?null:""}he&&Le(n.firstChild);for(var l=_e(n);r=l.nextNode();)3===r.nodeType&&r===i||Fe(r)||(r.content instanceof L&&He(r.content),ze(r),i=r);if(ge){if(ye)for(a=X.call(n.ownerDocument);n.firstChild;)a.appendChild(n.firstChild);else a=n;return ve&&(a=V.call(k,a,!0)),a}return pe?n.outerHTML:n.innerHTML},S.setConfig=function(e){Oe(e),fe=!0},S.clearConfig=function(){we=null,fe=!1},S.addHook=function(e,t){"function"==typeof t&&(Y[e]=Y[e]||[],Y[e].push(t))},S.removeHook=function(e){Y[e]&&Y[e].pop()},S.removeHooks=function(e){Y[e]&&(Y[e]=[])},S.removeAllHooks=function(){Y={}},S}var r=["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"],i=["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","audio","canvas","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","video","view","vkern"],a=["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feMerge","feMergeNode","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence"],l=["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmuliscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mpspace","msqrt","mystyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover"],s=["#text"],c=["accept","action","align","alt","autocomplete","background","bgcolor","border","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","coords","crossorigin","datetime","default","dir","disabled","download","enctype","face","for","headers","height","hidden","high","href","hreflang","id","integrity","ismap","label","lang","list","loop","low","max","maxlength","media","method","min","multiple","name","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","type","usemap","valign","value","width","xmlns"],d=["accent-height","accumulate","additivive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","tabindex","targetx","targety","transform","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"],u=["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"],m=["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"],p=/\{\{[\s\S]*|[\s\S]*\}\}/gm,f=/<%[\s\S]*|[\s\S]*%>/gm,h=/^data-[\-\w.\u00B7-\uFFFF]/,g=/^aria-[\-\w]+$/,y=/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,v=/^(?:\w+script|data):/i,b=/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g,T="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},A=function(){return"undefined"==typeof window?null:window};return o()}); diff --git a/js/test.js b/js/test.js deleted file mode 100644 index c6e5c7a..0000000 --- a/js/test.js +++ /dev/null @@ -1,673 +0,0 @@ -'use strict'; -var jsc = require('jsverify'), - jsdom = require('jsdom-global'), - cleanup = jsdom(), - - a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m', - 'n','o','p','q','r','s','t','u','v','w','x','y','z'], - alnumString = a2zString.concat(['0','1','2','3','4','5','6','7','8','9']), - queryString = alnumString.concat(['+','%','&','.','*','-','_']), - base64String = alnumString.concat(['+','/','=']).concat( - a2zString.map(function(c) { - return c.toUpperCase(); - }) - ), - // schemas supported by the whatwg-url library - schemas = ['ftp','gopher','http','https','ws','wss'], - supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], - logFile = require('fs').createWriteStream('test.log'); - -global.$ = global.jQuery = require('./jquery-3.1.1'); -global.sjcl = require('./sjcl-1.0.6'); -global.Base64 = require('./base64-2.1.9').Base64; -global.RawDeflate = require('./rawdeflate-0.5').RawDeflate; -global.RawDeflate.inflate = require('./rawinflate-0.3').RawDeflate.inflate; -require('./privatebin'); - -// redirect console messages to log file -console.warn = console.error = function (msg) { - logFile.write(msg + '\n'); -} - -describe('Helper', function () { - describe('secondsToHuman', function () { - after(function () { - cleanup(); - }); - - jsc.property('returns an array with a number and a word', 'integer', function (number) { - var result = $.PrivateBin.Helper.secondsToHuman(number); - return Array.isArray(result) && - result.length === 2 && - result[0] === parseInt(result[0], 10) && - typeof result[1] === 'string'; - }); - jsc.property('returns seconds on the first array position', 'integer 59', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[0] === number; - }); - jsc.property('returns seconds on the second array position', 'integer 59', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'second'; - }); - jsc.property('returns minutes on the first array position', 'integer 60 3599', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60); - }); - jsc.property('returns minutes on the second array position', 'integer 60 3599', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'minute'; - }); - jsc.property('returns hours on the first array position', 'integer 3600 86399', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60)); - }); - jsc.property('returns hours on the second array position', 'integer 3600 86399', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'hour'; - }); - jsc.property('returns days on the first array position', 'integer 86400 5184000', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24)); - }); - jsc.property('returns days on the second array position', 'integer 86400 5184000', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'day'; - }); - // max safe integer as per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5 - jsc.property('returns months on the first array position', 'integer 5184000 9007199254740991', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30)); - }); - jsc.property('returns months on the second array position', 'integer 5184000 9007199254740991', function (number) { - return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'month'; - }); - }); - - // 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 += '

' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '
'; - }); - 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 += '
' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '
'; - }); - var elements = $('').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 = $('
' + content + '
'), - 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 = $('
' + prefix + url + postfix + '
'); - - // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. � 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 = $('
' + prefix + url + '
'); - } - - $.PrivateBin.Helper.urls2links(element); - return element.html() === $('
' + prefix + '' + url + '' + postfix + '
').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 = $('
' + prefix + url + ' ' + postfix + '
'); - $.PrivateBin.Helper.urls2links(element); - return element.html() === $('
' + prefix + '' + url + ' ' + postfix + '
').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) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - params[0] = params[0].replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var 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) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var 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) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var 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) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - middle = middle.replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var 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) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - middle = middle.replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var 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) { - // deliberatly using a non-ascii key for replacing invalid characters - var key = item.replace(/[\s;,=]/g, Array(i+2).join('£')), - value = (values[i] || values[0]).replace(/[\s;,=]/g, ''); - cookieArray.push(key + '=' + value); - if (Math.random() < 1 / i || selectedKey === key) - { - 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(); - }); - - jsc.property( - 'returns the URL without query & fragment', - jsc.elements(schemas), - jsc.nearray(jsc.elements(a2zString)), - jsc.array(jsc.elements(queryString)), - 'string', - function (schema, address, query, fragment) { - var expected = schema + '://' + address.join('') + '/', - clean = jsdom('', {url: expected + '?' + query.join('') + '#' + fragment}), - result = $.PrivateBin.Helper.baseUri(); - $.PrivateBin.Helper.reset(); - clean(); - return expected === result; - } - ); - }); - - describe('htmlEntities', function () { - after(function () { - cleanup(); - }); - - jsc.property( - 'removes all HTML entities from any given string', - 'string', - function (string) { - var result = $.PrivateBin.Helper.htmlEntities(string); - return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); - } - ); - }); -}); - -describe('I18n', function () { - describe('translate', function () { - before(function () { - $.PrivateBin.I18n.reset(); - }); - - jsc.property( - 'returns message ID unchanged if no translation found', - 'string', - function (messageId) { - messageId = messageId.replace(/%(s|d)/g, '%%'); - var plurals = [messageId, messageId + 's'], - fake = [messageId], - result = $.PrivateBin.I18n.translate(messageId); - $.PrivateBin.I18n.reset(); - - var alias = $.PrivateBin.I18n._(messageId); - $.PrivateBin.I18n.reset(); - - var p_result = $.PrivateBin.I18n.translate(plurals); - $.PrivateBin.I18n.reset(); - - var p_alias = $.PrivateBin.I18n._(plurals); - $.PrivateBin.I18n.reset(); - - var f_result = $.PrivateBin.I18n.translate(fake); - $.PrivateBin.I18n.reset(); - - var f_alias = $.PrivateBin.I18n._(fake); - $.PrivateBin.I18n.reset(); - - return messageId === result && messageId === alias && - messageId === p_result && messageId === p_alias && - messageId === f_result && messageId === f_alias; - } - ); - jsc.property( - 'replaces %s in strings with first given parameter', - 'string', - '(small nearray) string', - 'string', - function (prefix, params, postfix) { - prefix = prefix.replace(/%(s|d)/g, '%%'); - params[0] = params[0].replace(/%(s|d)/g, '%%'); - postfix = postfix.replace(/%(s|d)/g, '%%'); - var translation = prefix + params[0] + postfix; - params.unshift(prefix + '%s' + postfix); - var result = $.PrivateBin.I18n.translate.apply(this, params); - $.PrivateBin.I18n.reset(); - var alias = $.PrivateBin.I18n._.apply(this, params); - $.PrivateBin.I18n.reset(); - return translation === result && translation === alias; - } - ); - }); - - describe('getPluralForm', function () { - before(function () { - $.PrivateBin.I18n.reset(); - }); - - jsc.property( - 'returns valid key for plural form', - jsc.elements(supportedLanguages), - 'integer', - function(language, n) { - $.PrivateBin.I18n.reset(language); - var result = $.PrivateBin.I18n.getPluralForm(n); - // arabic seems to have the highest plural count with 6 forms - return result >= 0 && result <= 5; - } - ); - }); - - // loading of JSON via AJAX needs to be tested in the browser, this just mocks it - // TODO: This needs to be tested using a browser. - describe('loadTranslations', function () { - before(function () { - $.PrivateBin.I18n.reset(); - }); - - jsc.property( - 'downloads and handles any supported language', - jsc.elements(supportedLanguages), - function(language) { - var clean = jsdom('', {url: 'https://privatebin.net/', cookie: ['lang=' + language]}); - - $.PrivateBin.I18n.reset('en'); - $.PrivateBin.I18n.loadTranslations(); - $.PrivateBin.I18n.reset(language, require('../i18n/' + language + '.json')); - var result = $.PrivateBin.I18n.translate('en'), - alias = $.PrivateBin.I18n._('en'); - - clean(); - return language === result && language === alias; - } - ); - }); -}); - -describe('CryptTool', function () { - describe('cipher & decipher', function () { - this.timeout(30000); - it('can en- and decrypt any message', function () { - jsc.check(jsc.forall( - 'string', - 'string', - 'string', - function (key, password, message) { - return message === $.PrivateBin.CryptTool.decipher( - key, - password, - $.PrivateBin.CryptTool.cipher(key, password, message) - ); - } - ), - // reducing amount of checks as running 100 takes about 5 minutes - {tests: 5, quiet: true}); - }); - - // The below static unit tests are included to ensure deciphering of "classic" - // SJCL based pastes still works - it( - 'supports PrivateBin v1 ciphertext (SJCL & Base64 2.1.9)', - function () { - // Of course you can easily decipher the following texts, if you like. - // Bonus points for finding their sources and hidden meanings. - var paste1 = $.PrivateBin.CryptTool.decipher( - '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', - // -- "That's amazing. I've got the same combination on my luggage." - Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''), - '{"iv":"4HNFIl7eYbCh6HuShctTIA==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"u0lQvePq6L0=","ct":"fGPUVrDyaVr1ZDGb+kqQ3CPEW8x4YKGfzHDmA0Vjkh250aWNe7Cnigkps9aaFVMX9AaerrTp3yZbojJtNqVGMfLdUTu+53xmZHqRKxCCqSfDNSNoW4Oxk5OVgAtRyuG4bXHDsWTXDNz2xceqzVFqhkwTwlUchrV7uuFK/XUKTNjPFM744moivIcBbfM2FOeKlIFs8RYPYuvqQhp2rMLlNGwwKh//4kykQsHMQDeSDuJl8stMQzgWR/btUBZuwNZEydkMH6IPpTdf5WTSrZ+wC2OK0GutCm4UaEe6txzaTMfu+WRVu4PN6q+N+2zljWJ1XdpVcN/i0Sv4QVMym0Xa6y0eccEhj/69o47PmExmMMeEwExImPalMNT9JUSiZdOZJ/GdzwrwoIuq1mdQR6vSH+XJ/8jXJQ7bjjJVJYXTcT0Di5jixArI2Kpp1GGlGVFbLgPugwU1wczg+byqeDOAECXRRnQcogeaJtVcRwXwfy4j3ORFcblYMilxyHqKBewcYPRVBGtBs50cVjSIkAfR84rnc1nfvnxK/Gmm+4VBNHI6ODWNpRolVMCzXjbKYnV3Are5AgSpsTqaGl41VJGpcco6cAwi4K0Bys1seKR+bLSdUgqRrkEqSRSdu3/VTu9HhEk8an0rjTE4CBB5/LMn16p0TGLoOb32odKFIEtpanVvLjeyiVMvSxcgYLNnTi/5FiaAC4pJxRD+AZHedU1FICUeEXxIcac/4E5qjkHjX9SpQtLl80QLIVnjNliZm7QLB/nKu7W8Jb0+/CiTdV3Q9LhxlH4ciprnX+W0B00BKYFHnL9jRVzKdXhf1EHydbXMAfpCjHAXIVCkFakJinQBDIIw/SC6Yig0u0ddEID2B7LYAP1iE4RZwzTrxCB+ke2jQr8c20Jj6u6ShFOPC9DCw9XupZ4HAalVG00kSgjus+b8zrVji3/LKEhb4EBzp1ctBJCFTeXwej8ZETLoXTylev5dlwZSYAbuBPPcbFR/xAIPx3uDabd1E1gTqUc68ICIGhd197Mb2eRWiSvHr5SPsASerMxId6XA6+iQlRiI+NDR+TGVNmCnfxSlyPFMOHGTmslXOGIqGfBR8l4ft8YVZ70lCwmwTuViGc75ULSf9mM57/LmRzQFMYQtvI8IFK9JaQEMY5xz0HLtR4iyQUUdwR9e0ytBNdWF2a2WPDEnJuY/QJo4GzTlgv4QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}' - ), - paste2 = $.PrivateBin.CryptTool.decipher( - 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', - '', // no password - '{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"jN6CjbQMJCM=","ct":"kYYMo5DFG1+w0UHiYXT5pdV0IUuXxzOlslkW/c3DRCbGFROCVkAskHce7HoRczee1N9c5MhHjVMJUIZE02qIS8UyHdJ/GqcPVidTUcj9rnDNWsTXkjVv8jCwHS/cwmAjDTWpwp5ThECN+ov/wNp/NdtTj8Qj7f/T3rfZIOCWfwLH9s4Des35UNcUidfPTNQ1l0Gm0X+r98CCUSYZjQxkZc6hRZBLPQ8EaNVooUwd5eP4GiYlmSDNA0wOSA+5isPYxomVCt+kFf58VBlNhpfNi7BLYAUTPpXT4SfH5drR9+C7NTeZ+tTCYjbU94PzYItOpu8vgnB1/a6BAM5h3m9w+giUb0df4hgTWeZnZxLjo5BN8WV+kdTXMj3/Vv0gw0DQrDcCuX/cBAjpy3lQGwlAN1vXoOIyZJUjMpQRrOLdKvLB+zcmVNtGDbgnfP2IYBzk9NtodpUa27ne0T0ZpwOPlVwevsIVZO224WLa+iQmmHOWDFFpVDlS0t0fLfOk7Hcb2xFsTxiCIiyKMho/IME1Du3X4e6BVa3hobSSZv0rRtNgY1KcyYPrUPW2fxZ+oik3y9SgGvb7XpjVIta8DWlDWRfZ9kzoweWEYqz9IA8Xd373RefpyuWI25zlHoX3nwljzsZU6dC//h/Dt2DNr+IAvKO3+u23cWoB9kgcZJ2FJuqjLvVfCF+OWcig7zs2pTYJW6Rg6lqbBCxiUUlae6xJrjfv0pzD2VYCLY7v1bVTagppwKzNI3WaluCOrdDYUCxUSe56yd1oAoLPRVbYvomRboUO6cjQhEknERyvt45og2kORJOEJayHW+jZgR0Y0jM3Nk17ubpij2gHxNx9kiLDOiCGSV5mn9mV7qd3HHcOMSykiBgbyzjobi96LT2dIGLeDXTIdPOog8wyobO4jWq0GGs0vBB8oSYXhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZMZtmnYpGAtAPg7AUG"}' - ); - - if (!paste1.includes('securely packed in iron') || !paste2.includes('Sol is right')) { - throw Error('v1 (SJCL based) pastes could not be deciphered'); - } - } - ); - - it( - 'supports ZeroBin ciphertext (SJCL & Base64 1.7)', - function () { - var newBase64 = global.Base64; - global.Base64 = require('./base64-1.7').Base64; - jsdom(); - delete require.cache[require.resolve('./privatebin')]; - require('./privatebin'); - - // Of course you can easily decipher the following texts, if you like. - // Bonus points for finding their sources and hidden meanings. - var paste1 = $.PrivateBin.CryptTool.decipher( - '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', - // -- "That's amazing. I've got the same combination on my luggage." - Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''), - '{"iv":"aTnR2qBL1CAmLX8FdWe3VA==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"u0lQvePq6L0=","ct":"A3nBTvICZtYy6xqbIJE0c8Veored5lMJUGgGUm4581wjrPFlU0Q0tUZSf+RUUoZj2jqDa4kiyyZ5YNMe30hNMV0oVSalNhRgD9svVMnPuF162IbyhVCwr7ULjT981CHxVlGNqGqmIU6L/XixgdArxAA8x1GCrfAkBWWGeq8Qw5vJPG/RCHpwR4Wy3azrluqeyERBzmaOQjO/kM35TiI6IrLYFyYyL7upYlxAaxS0XBMZvN8QU8Lnerwvh5JVC6OkkKrhogajTJIKozCF79yI78c50LUh7tTuI3Yoh7+fXxhoODvQdYFmoiUlrutN7Y5ZMRdITvVu8fTYtX9c7Fiufmcq5icEimiHp2g1bvfpOaGOsFT+XNFgC9215jcp5mpBdN852xs7bUtw+nDrf+LsDEX6iRpRZ+PYgLDN5xQT1ByEtYbeP+tO38pnx72oZdIB3cj8UkOxnxdNiZM5YB5egn4jUj1fHot1I69WoTiUJipZ5PIATv7ScymRB+AYzjxjurQ9lVfX9QtAbEH2dhdmoUo3IDRSXpWNCe9RC1aUIyWfZO7oI7FEohNscHNTLEcT+wFnFUPByLlXmjNZ7FKeNpvUm3jTY4t4sbZH8o2dUl624PAw1INcJ6FKqWGWwoFT2j1MYC+YV/LkLTdjuWfayvwLMh27G/FfKCRbW36vqinegqpPDylsx9+3oFkEw3y5Z8+44oN91rE/4Md7JhPJeRVlFC9TNCj4dA+EVhbbQqscvSnIH2uHkMw7mNNo7xba/YT9KoPDaniqnYqb+q2pX1WNWE7dLS2wfroMAS3kh8P22DAV37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAxSxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}' - ), - paste2 = $.PrivateBin.CryptTool.decipher( - 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', - '', // no password - '{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"jN6CjbQMJCM=","ct":"PuOPWB3i2FPcreSrLYeQf84LdE8RHjsc+MGtiOr4b7doNyWKYtkNorbRadxaPnEee2/Utrp1MIIfY5juJSy8RGwEPX5ciWcYe6EzsXWznsnvhmpKNj9B7eIIrfSbxfy8E2e/g7xav1nive+ljToka3WT1DZ8ILQd/NbnJeHWaoSEOfvz8+d8QJPb1tNZvs7zEY95DumQwbyOsIMKAvcZHJ9OJNpujXzdMyt6DpcFcqlldWBZ/8q5rAUTw0HNx/rCgbhAxRYfNoTLIcMM4L0cXbPSgCjwf5FuO3EdE13mgEDhcClW79m0QvcnIh8xgzYoxLbp0+AwvC/MbZM8savN/0ieWr2EKkZ04ggiOIEyvfCUuNprQBYO+y8kKduNEN6by0Yf4LRCPfmwN+GezDLuzTnZIMhPbGqUAdgV6ExqK2ULEEIrQEMoOuQIxfoMhqLlzG79vXGt2O+BY+4IiYfvmuRLks4UXfyHqxPXTJg48IYbGs0j4TtJPUgp3523EyYLwEGyVTAuWhYAmVIwd/hoV7d7tmfcF73w9dufDFI3LNca2KxzBnWNPYvIZKBwWbq8ncxkb191dP6mjEi7NnhqVk5A6vIBbu4AC5PZf76l6yep4xsoy/QtdDxCMocCXeAML9MQ9uPQbuspOKrBvMfN5igA1kBqasnxI472KBNXsdZnaDddSVUuvhTcETM="}' - ); - - global.Base64 = newBase64; - jsdom(); - delete require.cache[require.resolve('./privatebin')]; - require('./privatebin'); - if (!paste1.includes('securely packed in iron') || !paste2.includes('Sol is right')) { - throw Error('v1 (SJCL based) pastes could not be deciphered'); - } - } - ); - }); - - describe('isEntropyReady & addEntropySeedListener', function () { - it( - 'lets us know that enough entropy is collected or make us wait for it', - function(done) { - if ($.PrivateBin.CryptTool.isEntropyReady()) { - done(); - } else { - $.PrivateBin.CryptTool.addEntropySeedListener(function() { - done(); - }); - } - } - ); - }); - - describe('getSymmetricKey', function () { - var keys = []; - - // the parameter is used to ensure the test is run more then one time - jsc.property( - 'returns random, non-empty keys', - 'nat', - function(n) { - var key = $.PrivateBin.CryptTool.getSymmetricKey(), - result = (key !== '' && keys.indexOf(key) === -1); - keys.push(key); - return result; - } - ); - }); - - describe('Base64.js vs SJCL.js vs abab.js', function () { - jsc.property( - 'these all return the same base64 string', - 'string', - function(string) { - var base64 = Base64.toBase64(string), - sjcl = global.sjcl.codec.base64.fromBits(global.sjcl.codec.utf8String.toBits(string)), - abab = window.btoa(Base64.utob(string)); - return base64 === sjcl && sjcl === abab; - } - ); - }); -}); - -describe('Model', function () { - describe('getPasteId', function () { - before(function () { - $.PrivateBin.Model.reset(); - cleanup(); - }); - - jsc.property( - 'returns the query string without separator, if any', - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(queryString)), - 'string', - function (schema, address, query, fragment) { - var queryString = query.join(''), - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + queryString + '#' + fragment - }), - result = $.PrivateBin.Model.getPasteId(); - $.PrivateBin.Model.reset(); - clean(); - return queryString === result; - } - ); - jsc.property( - 'throws exception on empty query string', - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(a2zString)), - 'string', - function (schema, address, fragment) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/#' + fragment - }), - result = false; - try { - $.PrivateBin.Model.getPasteId(); - } - catch(err) { - result = true; - } - $.PrivateBin.Model.reset(); - clean(); - return result; - } - ); - }); - - describe('getPasteKey', function () { - jsc.property( - 'returns the fragment of the URL', - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(a2zString)), - jsc.array(jsc.elements(queryString)), - jsc.nearray(jsc.elements(base64String)), - function (schema, address, query, fragment) { - var fragmentString = fragment.join(''), - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') + '#' + fragmentString - }), - result = $.PrivateBin.Model.getPasteKey(); - $.PrivateBin.Model.reset(); - clean(); - return fragmentString === result; - } - ); - jsc.property( - 'returns the fragment stripped of trailing query parts', - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(a2zString)), - jsc.array(jsc.elements(queryString)), - jsc.nearray(jsc.elements(base64String)), - jsc.array(jsc.elements(queryString)), - function (schema, address, query, fragment, trail) { - var fragmentString = fragment.join(''), - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + '/?' + - query.join('') + '#' + fragmentString + '&' + trail.join('') - }), - result = $.PrivateBin.Model.getPasteKey(); - $.PrivateBin.Model.reset(); - clean(); - return fragmentString === result; - } - ); - jsc.property( - 'throws exception on empty fragment of the URL', - jsc.nearray(jsc.elements(a2zString)), - jsc.nearray(jsc.elements(a2zString)), - jsc.array(jsc.elements(queryString)), - function (schema, address, query) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') - }), - result = false; - try { - $.PrivateBin.Model.getPasteKey(); - } - catch(err) { - result = true; - } - $.PrivateBin.Model.reset(); - clean(); - return result; - } - ); - }); -}); diff --git a/js/test/Alert.js b/js/test/Alert.js new file mode 100644 index 0000000..617c488 --- /dev/null +++ b/js/test/Alert.js @@ -0,0 +1,226 @@ +'use strict'; +var common = require('../common'); + +describe('Alert', function () { + describe('showStatus', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows a status message', + jsc.array(common.jscAlnumString()), + jsc.array(common.jscAlnumString()), + function (icon, message) { + icon = icon.join(''); + message = message.join(''); + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showStatus(message, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showError', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows an error message', + jsc.array(common.jscAlnumString()), + jsc.array(common.jscAlnumString()), + function (icon, message) { + icon = icon.join(''); + message = message.join(''); + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showError(message, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showRemaining', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows remaining time', + jsc.array(common.jscAlnumString()), + jsc.array(common.jscAlnumString()), + 'integer', + function (message, string, number) { + message = message.join(''); + string = string.join(''); + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showLoading', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows a loading message', + jsc.array(common.jscAlnumString()), + jsc.array(common.jscAlnumString()), + function (message, icon) { + message = message.join(''); + icon = icon.join(''); + var defaultMessage = 'Loading…'; + if (message.length === 0) { + message = defaultMessage; + } + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showLoading(message, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('hideLoading', function () { + before(function () { + cleanup(); + }); + + it( + 'hides the loading message', + function() { + $('body').html( + '' + ); + $('body').addClass('loading'); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.hideLoading(); + assert.ok( + !$('body').hasClass('loading') && + $('#loadingindicator').hasClass('hidden') + ); + } + ); + }); + + describe('hideMessages', function () { + before(function () { + cleanup(); + }); + + it( + 'hides all messages', + function() { + $('body').html( + '' + + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.hideMessages(); + assert.ok( + $('#status').hasClass('hidden') && + $('#errormessage').hasClass('hidden') + ); + } + ); + }); + + describe('setCustomHandler', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'calls a given handler function', + 'nat 3', + jsc.array(common.jscAlnumString()), + function (trigger, message) { + message = message.join(''); + var handlerCalled = false, + defaultMessage = 'Loading…', + functions = [ + $.PrivateBin.Alert.showStatus, + $.PrivateBin.Alert.showError, + $.PrivateBin.Alert.showRemaining, + $.PrivateBin.Alert.showLoading + ]; + if (message.length === 0) { + message = defaultMessage; + } + $('body').html( + '' + + '' + + '' + + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.setCustomHandler(function(id, $element) { + handlerCalled = true; + return jsc.random(0, 1) ? true : $element; + }); + functions[trigger](message); + return handlerCalled; + } + ); + }); +}); + diff --git a/js/test/AttachmentViewer.js b/js/test/AttachmentViewer.js new file mode 100644 index 0000000..c1495fb --- /dev/null +++ b/js/test/AttachmentViewer.js @@ -0,0 +1,97 @@ +'use strict'; +var common = require('../common'); + +describe('AttachmentViewer', function () { + describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'displays & hides data as requested', + common.jscMimeTypes(), + jsc.nearray(common.jscBase64String()), + 'string', + 'string', + 'string', + function (mimeType, base64, filename, prefix, postfix) { + var clean = jsdom(), + data = 'data:' + mimeType + ';base64,' + base64.join(''), + previewSupported = ( + mimeType.substring(0, 6) === 'image/' || + mimeType.substring(0, 6) === 'audio/' || + mimeType.substring(0, 6) === 'video/' || + mimeType.match(/\/pdf/i) + ), + results = []; + prefix = prefix.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + $('body').html( + '' + ); + $.PrivateBin.AttachmentViewer.init(); + results.push( + !$.PrivateBin.AttachmentViewer.hasAttachment() && + $('#attachment').hasClass('hidden') && + $('#attachmentPreview').hasClass('hidden') + ); + if (filename.length) { + $.PrivateBin.AttachmentViewer.setAttachment(data, filename); + } else { + $.PrivateBin.AttachmentViewer.setAttachment(data); + } + var attachment = $.PrivateBin.AttachmentViewer.getAttachment(); + results.push( + $.PrivateBin.AttachmentViewer.hasAttachment() && + $('#attachment').hasClass('hidden') && + $('#attachmentPreview').hasClass('hidden') && + attachment[0] === data && + attachment[1] === filename + ); + $.PrivateBin.AttachmentViewer.showAttachment(); + results.push( + !$('#attachment').hasClass('hidden') && + (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden')) + ); + $.PrivateBin.AttachmentViewer.hideAttachment(); + results.push( + $('#attachment').hasClass('hidden') && + (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden')) + ); + if (previewSupported) { + $.PrivateBin.AttachmentViewer.hideAttachmentPreview(); + results.push($('#attachmentPreview').hasClass('hidden')); + } + $.PrivateBin.AttachmentViewer.showAttachment(); + results.push( + !$('#attachment').hasClass('hidden') && + (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden')) + ); + var element = $('
'); + $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, prefix + '%s' + postfix); + if (filename.length) { + results.push( + element.children()[0].href === data && + element.children()[0].getAttribute('download') === filename && + element.children()[0].text === prefix + filename + postfix + ); + } else { + results.push(element.children()[0].href === data); + } + $.PrivateBin.AttachmentViewer.removeAttachment(); + results.push( + $('#attachment').hasClass('hidden') && + $('#attachmentPreview').hasClass('hidden') + ); + clean(); + return results.every(element => element); + } + ); + }); +}); + diff --git a/js/test/CryptTool.js b/js/test/CryptTool.js new file mode 100644 index 0000000..3c89fff --- /dev/null +++ b/js/test/CryptTool.js @@ -0,0 +1,207 @@ +'use strict'; +require('../common'); + +describe('CryptTool', function () { + describe('cipher & decipher', function () { + this.timeout(30000); + it('can en- and decrypt any message', function () { + jsc.check(jsc.forall( + 'string', + 'string', + 'string', + function (key, password, message) { + return message === $.PrivateBin.CryptTool.decipher( + key, + password, + $.PrivateBin.CryptTool.cipher(key, password, message) + ); + } + ), + // reducing amount of checks as running 100 takes about 5 minutes + {tests: 5, quiet: true}); + }); + + // The below static unit tests are included to ensure deciphering of "classic" + // SJCL based pastes still works + it( + 'supports PrivateBin v1 ciphertext (SJCL & Base64 2.1.9)', + function () { + // Of course you can easily decipher the following texts, if you like. + // Bonus points for finding their sources and hidden meanings. + var paste1 = $.PrivateBin.CryptTool.decipher( + '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', + // -- "That's amazing. I've got the same combination on my luggage." + Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''), + '{"iv":"4HNFIl7eYbCh6HuShctTIA==","v":1,"iter":10000,"ks"' + + ':256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","sa' + + 'lt":"u0lQvePq6L0=","ct":"fGPUVrDyaVr1ZDGb+kqQ3CPEW8x4YKG' + + 'fzHDmA0Vjkh250aWNe7Cnigkps9aaFVMX9AaerrTp3yZbojJtNqVGMfL' + + 'dUTu+53xmZHqRKxCCqSfDNSNoW4Oxk5OVgAtRyuG4bXHDsWTXDNz2xce' + + 'qzVFqhkwTwlUchrV7uuFK/XUKTNjPFM744moivIcBbfM2FOeKlIFs8RY' + + 'PYuvqQhp2rMLlNGwwKh//4kykQsHMQDeSDuJl8stMQzgWR/btUBZuwNZ' + + 'EydkMH6IPpTdf5WTSrZ+wC2OK0GutCm4UaEe6txzaTMfu+WRVu4PN6q+' + + 'N+2zljWJ1XdpVcN/i0Sv4QVMym0Xa6y0eccEhj/69o47PmExmMMeEwEx' + + 'ImPalMNT9JUSiZdOZJ/GdzwrwoIuq1mdQR6vSH+XJ/8jXJQ7bjjJVJYX' + + 'TcT0Di5jixArI2Kpp1GGlGVFbLgPugwU1wczg+byqeDOAECXRRnQcoge' + + 'aJtVcRwXwfy4j3ORFcblYMilxyHqKBewcYPRVBGtBs50cVjSIkAfR84r' + + 'nc1nfvnxK/Gmm+4VBNHI6ODWNpRolVMCzXjbKYnV3Are5AgSpsTqaGl4' + + '1VJGpcco6cAwi4K0Bys1seKR+bLSdUgqRrkEqSRSdu3/VTu9HhEk8an0' + + 'rjTE4CBB5/LMn16p0TGLoOb32odKFIEtpanVvLjeyiVMvSxcgYLNnTi/' + + '5FiaAC4pJxRD+AZHedU1FICUeEXxIcac/4E5qjkHjX9SpQtLl80QLIVn' + + 'jNliZm7QLB/nKu7W8Jb0+/CiTdV3Q9LhxlH4ciprnX+W0B00BKYFHnL9' + + 'jRVzKdXhf1EHydbXMAfpCjHAXIVCkFakJinQBDIIw/SC6Yig0u0ddEID' + + '2B7LYAP1iE4RZwzTrxCB+ke2jQr8c20Jj6u6ShFOPC9DCw9XupZ4HAal' + + 'VG00kSgjus+b8zrVji3/LKEhb4EBzp1ctBJCFTeXwej8ZETLoXTylev5' + + 'dlwZSYAbuBPPcbFR/xAIPx3uDabd1E1gTqUc68ICIGhd197Mb2eRWiSv' + + 'Hr5SPsASerMxId6XA6+iQlRiI+NDR+TGVNmCnfxSlyPFMOHGTmslXOGI' + + 'qGfBR8l4ft8YVZ70lCwmwTuViGc75ULSf9mM57/LmRzQFMYQtvI8IFK9' + + 'JaQEMY5xz0HLtR4iyQUUdwR9e0ytBNdWF2a2WPDEnJuY/QJo4GzTlgv4' + + 'QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9' + + 'C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5' + + 'imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}' + ), + paste2 = $.PrivateBin.CryptTool.decipher( + 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', + '', // no password + '{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks"' + + ':256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","sa' + + 'lt":"jN6CjbQMJCM=","ct":"kYYMo5DFG1+w0UHiYXT5pdV0IUuXxzO' + + 'lslkW/c3DRCbGFROCVkAskHce7HoRczee1N9c5MhHjVMJUIZE02qIS8U' + + 'yHdJ/GqcPVidTUcj9rnDNWsTXkjVv8jCwHS/cwmAjDTWpwp5ThECN+ov' + + '/wNp/NdtTj8Qj7f/T3rfZIOCWfwLH9s4Des35UNcUidfPTNQ1l0Gm0X+' + + 'r98CCUSYZjQxkZc6hRZBLPQ8EaNVooUwd5eP4GiYlmSDNA0wOSA+5isP' + + 'YxomVCt+kFf58VBlNhpfNi7BLYAUTPpXT4SfH5drR9+C7NTeZ+tTCYjb' + + 'U94PzYItOpu8vgnB1/a6BAM5h3m9w+giUb0df4hgTWeZnZxLjo5BN8WV' + + '+kdTXMj3/Vv0gw0DQrDcCuX/cBAjpy3lQGwlAN1vXoOIyZJUjMpQRrOL' + + 'dKvLB+zcmVNtGDbgnfP2IYBzk9NtodpUa27ne0T0ZpwOPlVwevsIVZO2' + + '24WLa+iQmmHOWDFFpVDlS0t0fLfOk7Hcb2xFsTxiCIiyKMho/IME1Du3' + + 'X4e6BVa3hobSSZv0rRtNgY1KcyYPrUPW2fxZ+oik3y9SgGvb7XpjVIta' + + '8DWlDWRfZ9kzoweWEYqz9IA8Xd373RefpyuWI25zlHoX3nwljzsZU6dC' + + '//h/Dt2DNr+IAvKO3+u23cWoB9kgcZJ2FJuqjLvVfCF+OWcig7zs2pTY' + + 'JW6Rg6lqbBCxiUUlae6xJrjfv0pzD2VYCLY7v1bVTagppwKzNI3WaluC' + + 'OrdDYUCxUSe56yd1oAoLPRVbYvomRboUO6cjQhEknERyvt45og2kORJO' + + 'EJayHW+jZgR0Y0jM3Nk17ubpij2gHxNx9kiLDOiCGSV5mn9mV7qd3HHc' + + 'OMSykiBgbyzjobi96LT2dIGLeDXTIdPOog8wyobO4jWq0GGs0vBB8oSY' + + 'XhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr' + + '99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZ' + + 'MZtmnYpGAtAPg7AUG"}' + ); + + assert.ok( + paste1.includes('securely packed in iron') && + paste2.includes('Sol is right') + ); + } + ); + + it( + 'supports ZeroBin ciphertext (SJCL & Base64 1.7)', + function () { + var newBase64 = global.Base64; + global.Base64 = require('../base64-1.7').Base64; + jsdom(); + delete require.cache[require.resolve('../privatebin')]; + require('../privatebin'); + + // Of course you can easily decipher the following texts, if you like. + // Bonus points for finding their sources and hidden meanings. + var paste1 = $.PrivateBin.CryptTool.decipher( + '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', + // -- "That's amazing. I've got the same combination on my luggage." + Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''), + '{"iv":"aTnR2qBL1CAmLX8FdWe3VA==","v":1,"iter":10000,"ks"' + + ':256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","sa' + + 'lt":"u0lQvePq6L0=","ct":"A3nBTvICZtYy6xqbIJE0c8Veored5lM' + + 'JUGgGUm4581wjrPFlU0Q0tUZSf+RUUoZj2jqDa4kiyyZ5YNMe30hNMV0' + + 'oVSalNhRgD9svVMnPuF162IbyhVCwr7ULjT981CHxVlGNqGqmIU6L/Xi' + + 'xgdArxAA8x1GCrfAkBWWGeq8Qw5vJPG/RCHpwR4Wy3azrluqeyERBzma' + + 'OQjO/kM35TiI6IrLYFyYyL7upYlxAaxS0XBMZvN8QU8Lnerwvh5JVC6O' + + 'kkKrhogajTJIKozCF79yI78c50LUh7tTuI3Yoh7+fXxhoODvQdYFmoiU' + + 'lrutN7Y5ZMRdITvVu8fTYtX9c7Fiufmcq5icEimiHp2g1bvfpOaGOsFT' + + '+XNFgC9215jcp5mpBdN852xs7bUtw+nDrf+LsDEX6iRpRZ+PYgLDN5xQ' + + 'T1ByEtYbeP+tO38pnx72oZdIB3cj8UkOxnxdNiZM5YB5egn4jUj1fHot' + + '1I69WoTiUJipZ5PIATv7ScymRB+AYzjxjurQ9lVfX9QtAbEH2dhdmoUo' + + '3IDRSXpWNCe9RC1aUIyWfZO7oI7FEohNscHNTLEcT+wFnFUPByLlXmjN' + + 'Z7FKeNpvUm3jTY4t4sbZH8o2dUl624PAw1INcJ6FKqWGWwoFT2j1MYC+' + + 'YV/LkLTdjuWfayvwLMh27G/FfKCRbW36vqinegqpPDylsx9+3oFkEw3y' + + '5Z8+44oN91rE/4Md7JhPJeRVlFC9TNCj4dA+EVhbbQqscvSnIH2uHkMw' + + '7mNNo7xba/YT9KoPDaniqnYqb+q2pX1WNWE7dLS2wfroMAS3kh8P22DA' + + 'V37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAx' + + 'SxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}' + ), + paste2 = $.PrivateBin.CryptTool.decipher( + 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', + '', // no password + '{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks"' + + ':256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","sa' + + 'lt":"jN6CjbQMJCM=","ct":"PuOPWB3i2FPcreSrLYeQf84LdE8RHjs' + + 'c+MGtiOr4b7doNyWKYtkNorbRadxaPnEee2/Utrp1MIIfY5juJSy8RGw' + + 'EPX5ciWcYe6EzsXWznsnvhmpKNj9B7eIIrfSbxfy8E2e/g7xav1nive+' + + 'ljToka3WT1DZ8ILQd/NbnJeHWaoSEOfvz8+d8QJPb1tNZvs7zEY95Dum' + + 'QwbyOsIMKAvcZHJ9OJNpujXzdMyt6DpcFcqlldWBZ/8q5rAUTw0HNx/r' + + 'CgbhAxRYfNoTLIcMM4L0cXbPSgCjwf5FuO3EdE13mgEDhcClW79m0Qvc' + + 'nIh8xgzYoxLbp0+AwvC/MbZM8savN/0ieWr2EKkZ04ggiOIEyvfCUuNp' + + 'rQBYO+y8kKduNEN6by0Yf4LRCPfmwN+GezDLuzTnZIMhPbGqUAdgV6Ex' + + 'qK2ULEEIrQEMoOuQIxfoMhqLlzG79vXGt2O+BY+4IiYfvmuRLks4UXfy' + + 'HqxPXTJg48IYbGs0j4TtJPUgp3523EyYLwEGyVTAuWhYAmVIwd/hoV7d' + + '7tmfcF73w9dufDFI3LNca2KxzBnWNPYvIZKBwWbq8ncxkb191dP6mjEi' + + '7NnhqVk5A6vIBbu4AC5PZf76l6yep4xsoy/QtdDxCMocCXeAML9MQ9uP' + + 'QbuspOKrBvMfN5igA1kBqasnxI472KBNXsdZnaDddSVUuvhTcETM="}' + ); + + global.Base64 = newBase64; + jsdom(); + delete require.cache[require.resolve('../privatebin')]; + require('../privatebin'); + assert.ok( + paste1.includes('securely packed in iron') && + paste2.includes('Sol is right') + ); + } + ); + }); + + describe('isEntropyReady & addEntropySeedListener', function () { + it( + 'lets us know that enough entropy is collected or make us wait for it', + function(done) { + if ($.PrivateBin.CryptTool.isEntropyReady()) { + done(); + } else { + $.PrivateBin.CryptTool.addEntropySeedListener(function() { + done(); + }); + } + } + ); + }); + + describe('getSymmetricKey', function () { + var keys = []; + + // the parameter is used to ensure the test is run more then one time + jsc.property( + 'returns random, non-empty keys', + function() { + var key = $.PrivateBin.CryptTool.getSymmetricKey(), + result = (key !== '' && keys.indexOf(key) === -1); + keys.push(key); + return result; + } + ); + }); + + describe('Base64.js vs SJCL.js vs abab.js', function () { + jsc.property( + 'these all return the same base64 string', + 'string', + function(string) { + var base64 = Base64.toBase64(string), + sjcl = global.sjcl.codec.base64.fromBits(global.sjcl.codec.utf8String.toBits(string)), + abab = window.btoa(Base64.utob(string)); + return base64 === sjcl && sjcl === abab; + } + ); + }); +}); + diff --git a/js/test/DiscussionViewer.js b/js/test/DiscussionViewer.js new file mode 100644 index 0000000..ff06c01 --- /dev/null +++ b/js/test/DiscussionViewer.js @@ -0,0 +1,116 @@ +'use strict'; +var common = require('../common'); + +describe('DiscussionViewer', function () { + describe('handleNotification, prepareNewDiscussion, addComment, finishDiscussion, getReplyMessage, getReplyNickname, getReplyCommentId & highlightComment', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'displays & hides comments as requested', + jsc.array( + jsc.record({ + idArray: jsc.nearray(common.jscAlnumString()), + parentidArray: jsc.nearray(common.jscAlnumString()), + data: jsc.string, + meta: jsc.record({ + nickname: jsc.string, + postdate: jsc.nat, + vizhash: jsc.string + }) + }) + ), + 'nat', + 'bool', + 'string', + 'string', + jsc.elements(['loading', 'danger', 'other']), + 'nestring', + function (comments, commentKey, fadeOut, nickname, message, alertType, alert) { + var clean = jsdom(), + results = []; + $('body').html( + '

Discussion

' + + '
' + + '
' + + '
name' + + '0000-00-00
' + + '
c
' + + '' + + '

' + + '' + + '

' + ); + $.PrivateBin.Model.init(); + $.PrivateBin.DiscussionViewer.init(); + results.push( + !$('#discussion').hasClass('hidden') + ); + $.PrivateBin.DiscussionViewer.prepareNewDiscussion(); + results.push( + $('#discussion').hasClass('hidden') + ); + comments.forEach(function (comment) { + comment.id = comment.idArray.join(''); + comment.parentid = comment.parentidArray.join(''); + $.PrivateBin.DiscussionViewer.addComment(comment, comment.data, comment.meta.nickname); + }); + results.push( + $('#discussion').hasClass('hidden') + ); + $.PrivateBin.DiscussionViewer.finishDiscussion(); + results.push( + !$('#discussion').hasClass('hidden') && + comments.length + 1 >= $('#commentcontainer').children().length + ); + if (comments.length > 0) { + if (commentKey >= comments.length) { + commentKey = commentKey % comments.length; + } + $.PrivateBin.DiscussionViewer.highlightComment(comments[commentKey].id, fadeOut); + results.push( + $('#comment_' + comments[commentKey].id).hasClass('highlight') + ); + } + $('#commentcontainer').find('button')[0].click(); + results.push( + !$('#reply').hasClass('hidden') + ); + $('#reply #nickname').val(nickname); + $('#reply #replymessage').val(message); + $.PrivateBin.DiscussionViewer.getReplyCommentId(); + results.push( + $.PrivateBin.DiscussionViewer.getReplyNickname() === $('#reply #nickname').val() && + $.PrivateBin.DiscussionViewer.getReplyMessage() === $('#reply #replymessage').val() + ); + var notificationResult = $.PrivateBin.DiscussionViewer.handleNotification(alertType === 'other' ? alert : alertType); + if (alertType === 'loading') { + results.push(notificationResult === false); + } else { + results.push( + alertType === 'danger' ? ( + notificationResult.hasClass('alert-danger') && + !notificationResult.hasClass('alert-info') + ) : ( + !notificationResult.hasClass('alert-danger') && + notificationResult.hasClass('alert-info') + ) + ); + } + clean(); + return results.every(element => element); + } + ); + }); +}); + diff --git a/js/test/Editor.js b/js/test/Editor.js new file mode 100644 index 0000000..86a9d9f --- /dev/null +++ b/js/test/Editor.js @@ -0,0 +1,74 @@ +'use strict'; +require('../common'); + +describe('Editor', function () { + describe('show, hide, getText, setText & isPreview', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'returns text fed into the textarea, handles editor tabs', + 'string', + function (text) { + var clean = jsdom(), + results = []; + $('body').html( + '' + + '

' + ); + $.PrivateBin.Editor.init(); + results.push( + $('#editorTabs').hasClass('hidden') && + $('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.show(); + results.push( + !$('#editorTabs').hasClass('hidden') && + !$('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.hide(); + results.push( + $('#editorTabs').hasClass('hidden') && + $('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.show(); + $.PrivateBin.Editor.focusInput(); + results.push( + $.PrivateBin.Editor.getText().length === 0 + ); + $.PrivateBin.Editor.setText(text); + results.push( + $.PrivateBin.Editor.getText() === $('#message').val() + ); + $.PrivateBin.Editor.setText(); + results.push( + !$.PrivateBin.Editor.isPreview() && + !$('#message').hasClass('hidden') + ); + $('#messagepreview').click(); + results.push( + $.PrivateBin.Editor.isPreview() && + $('#message').hasClass('hidden') + ); + $('#messageedit').click(); + results.push( + !$.PrivateBin.Editor.isPreview() && + !$('#message').hasClass('hidden') + ); + clean(); + return results.every(element => element); + } + ); + }); +}); + diff --git a/js/test/Helper.js b/js/test/Helper.js new file mode 100644 index 0000000..e4141f5 --- /dev/null +++ b/js/test/Helper.js @@ -0,0 +1,278 @@ +'use strict'; +var common = require('../common'); + +describe('Helper', function () { + describe('secondsToHuman', function () { + after(function () { + cleanup(); + }); + + jsc.property('returns an array with a number and a word', 'integer', function (number) { + var result = $.PrivateBin.Helper.secondsToHuman(number); + return Array.isArray(result) && + result.length === 2 && + result[0] === parseInt(result[0], 10) && + typeof result[1] === 'string'; + }); + jsc.property('returns seconds on the first array position', 'integer 59', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === number; + }); + jsc.property('returns seconds on the second array position', 'integer 59', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'second'; + }); + jsc.property('returns minutes on the first array position', 'integer 60 3599', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60); + }); + jsc.property('returns minutes on the second array position', 'integer 60 3599', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'minute'; + }); + jsc.property('returns hours on the first array position', 'integer 3600 86399', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60)); + }); + jsc.property('returns hours on the second array position', 'integer 3600 86399', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'hour'; + }); + jsc.property('returns days on the first array position', 'integer 86400 5184000', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24)); + }); + jsc.property('returns days on the second array position', 'integer 86400 5184000', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'day'; + }); + // max safe integer as per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5 + jsc.property('returns months on the first array position', 'integer 5184000 9007199254740991', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30)); + }); + jsc.property('returns months on the second array position', 'integer 5184000 9007199254740991', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'month'; + }); + }); + + // 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 () { + this.timeout(30000); + jsc.property( + 'selection contains content of given ID', + jsc.nearray(jsc.nearray(common.jscAlnumString())), + 'nearray string', + function (ids, contents) { + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + common.htmlEntities(contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet. + // Once there is one, uncomment the block below to actually check the result. + /* + ids.forEach(function(item, i) { + $.PrivateBin.Helper.selectText(item.join('')); + result *= (contents[i] || contents[0]) === window.getSelection().toString(); + }); + */ + clean(); + return Boolean(result); + } + ); + }); + + describe('urls2links', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'ignores non-URL content', + 'string', + function (content) { + return content === $.PrivateBin.Helper.urls2links(content); + } + ); + jsc.property( + 'replaces URLs with anchors', + 'string', + jsc.elements(['http', 'https', 'ftp']), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.array(common.jscQueryString()), + 'string', + function (prefix, schema, address, query, fragment, postfix) { + var query = query.join(''), + fragment = fragment.join(''), + url = schema + '://' + address.join('') + '/?' + query + '#' + fragment, + prefix = common.htmlEntities(prefix), + postfix = ' ' + common.htmlEntities(postfix); + + // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. � 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 = ''; + } + + return prefix + '' + url + '' + postfix === $.PrivateBin.Helper.urls2links(prefix + url + postfix); + } + ); + jsc.property( + 'replaces magnet links with anchors', + 'string', + jsc.array(common.jscQueryString()), + 'string', + function (prefix, query, postfix) { + var url = 'magnet:?' + query.join('').replace(/^&+|&+$/gm,''), + prefix = common.htmlEntities(prefix), + postfix = common.htmlEntities(postfix); + return prefix + '' + url + ' ' + postfix === $.PrivateBin.Helper.urls2links(prefix + url + ' ' + postfix); + } + ); + }); + + 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) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + params[0] = params[0].replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var 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) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var 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) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var 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) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + middle = middle.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var 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) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + middle = middle.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var 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 () { + this.timeout(30000); + jsc.property( + 'returns the requested cookie', + 'nearray asciinestring', + 'nearray asciistring', + function (labels, values) { + var selectedKey = '', selectedValue = '', + cookieArray = []; + labels.forEach(function(item, i) { + // deliberatly using a non-ascii key for replacing invalid characters + var key = item.replace(/[\s;,=]/g, Array(i+2).join('£')), + value = (values[i] || values[0]).replace(/[\s;,=]/g, ''); + cookieArray.push(key + '=' + value); + if (Math.random() < 1 / i || selectedKey === key) + { + selectedKey = key; + selectedValue = value; + } + }); + var clean = jsdom('', {cookie: cookieArray}), + result = $.PrivateBin.Helper.getCookie(selectedKey); + clean(); + return result === selectedValue; + } + ); + }); + + describe('baseUri', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'returns the URL without query & fragment', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + 'string', + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', {url: expected + '?' + query.join('') + '#' + fragment}), + result = $.PrivateBin.Helper.baseUri(); + $.PrivateBin.Helper.reset(); + clean(); + return expected === result; + } + ); + }); + + describe('htmlEntities', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'removes all HTML entities from any given string', + 'string', + function (string) { + var result = common.htmlEntities(string); + return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); + } + ); + }); +}); + diff --git a/js/test/I18n.js b/js/test/I18n.js new file mode 100644 index 0000000..10d333a --- /dev/null +++ b/js/test/I18n.js @@ -0,0 +1,128 @@ +'use strict'; +var common = require('../common'); + +describe('I18n', function () { + describe('translate', function () { + before(function () { + $.PrivateBin.I18n.reset(); + }); + + jsc.property( + 'returns message ID unchanged if no translation found', + 'string', + function (messageId) { + messageId = messageId.replace(/%(s|d)/g, '%%'); + var plurals = [messageId, messageId + 's'], + fake = [messageId], + result = $.PrivateBin.I18n.translate(messageId); + $.PrivateBin.I18n.reset(); + + var alias = $.PrivateBin.I18n._(messageId); + $.PrivateBin.I18n.reset(); + + var pluralResult = $.PrivateBin.I18n.translate(plurals); + $.PrivateBin.I18n.reset(); + + var pluralAlias = $.PrivateBin.I18n._(plurals); + $.PrivateBin.I18n.reset(); + + var fakeResult = $.PrivateBin.I18n.translate(fake); + $.PrivateBin.I18n.reset(); + + var fakeAlias = $.PrivateBin.I18n._(fake); + $.PrivateBin.I18n.reset(); + + return messageId === result && messageId === alias && + messageId === pluralResult && messageId === pluralAlias && + messageId === fakeResult && messageId === fakeAlias; + } + ); + jsc.property( + 'replaces %s in strings with first given parameter', + 'string', + '(small nearray) string', + 'string', + function (prefix, params, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + params[0] = params[0].replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var translation = prefix + params[0] + postfix; + params.unshift(prefix + '%s' + postfix); + var result = $.PrivateBin.I18n.translate.apply(this, params); + $.PrivateBin.I18n.reset(); + var alias = $.PrivateBin.I18n._.apply(this, params); + $.PrivateBin.I18n.reset(); + return translation === result && translation === alias; + } + ); + }); + + describe('getPluralForm', function () { + before(function () { + $.PrivateBin.I18n.reset(); + }); + + jsc.property( + 'returns valid key for plural form', + common.jscSupportedLanguages(), + 'integer', + function(language, n) { + $.PrivateBin.I18n.reset(language); + var result = $.PrivateBin.I18n.getPluralForm(n); + // arabic seems to have the highest plural count with 6 forms + return result >= 0 && result <= 5; + } + ); + }); + + // loading of JSON via AJAX needs to be tested in the browser, this just mocks it + // TODO: This needs to be tested using a browser. + describe('loadTranslations', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.I18n.reset(); + }); + + jsc.property( + 'downloads and handles any supported language', + common.jscSupportedLanguages(), + function(language) { + var clean = jsdom('', {url: 'https://privatebin.net/', cookie: ['lang=' + language]}); + + $.PrivateBin.I18n.reset('en'); + $.PrivateBin.I18n.loadTranslations(); + $.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json')); + var result = $.PrivateBin.I18n.translate('en'), + alias = $.PrivateBin.I18n._('en'); + + clean(); + return language === result && language === alias; + } + ); + + jsc.property( + 'should default to en', + function() { + var clean = jsdom('', {url: 'https://privatebin.net/'}); + + // when navigator.userLanguage is undefined and no default language + // is specified, it would throw an error + [ 'language', 'userLanguage' ].forEach(function (key) { + Object.defineProperty(navigator, key, { + value: undefined, + writeable: false + }); + }); + + $.PrivateBin.I18n.reset('en'); + $.PrivateBin.I18n.loadTranslations(); + var result = $.PrivateBin.I18n.translate('en'), + alias = $.PrivateBin.I18n._('en'); + + clean(); + return 'en' === result && 'en' === alias; + } + ); + }); +}); + diff --git a/js/test/Model.js b/js/test/Model.js new file mode 100644 index 0000000..8605476 --- /dev/null +++ b/js/test/Model.js @@ -0,0 +1,221 @@ +'use strict'; +var common = require('../common'); + +describe('Model', function () { + describe('getExpirationDefault', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "pasteExpiration"', + 'array asciinestring', + 'string', + 'small nat', + function (keys, value, key) { + keys = keys.map(common.htmlEntities); + value = common.htmlEntities(value); + var content = keys.length > key ? keys[key] : (keys.length > 0 ? keys[0] : 'null'), + contents = ''; + $('body').html(contents); + var result = common.htmlEntities( + $.PrivateBin.Model.getExpirationDefault() + ); + $.PrivateBin.Model.reset(); + return content === result; + } + ); + }); + + describe('getFormatDefault', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "pasteFormatter"', + 'array asciinestring', + 'string', + 'small nat', + function (keys, value, key) { + keys = keys.map(common.htmlEntities); + value = common.htmlEntities(value); + var content = keys.length > key ? keys[key] : (keys.length > 0 ? keys[0] : 'null'), + contents = ''; + $('body').html(contents); + var result = common.htmlEntities( + $.PrivateBin.Model.getFormatDefault() + ); + $.PrivateBin.Model.reset(); + return content === result; + } + ); + }); + + describe('getPasteId', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the query string without separator, if any', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscQueryString()), + 'string', + function (schema, address, query, fragment) { + var queryString = query.join(''), + clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/?' + queryString + '#' + fragment + }), + result = $.PrivateBin.Model.getPasteId(); + $.PrivateBin.Model.reset(); + clean(); + return queryString === result; + } + ); + jsc.property( + 'throws exception on empty query string', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + 'string', + function (schema, address, fragment) { + var clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/#' + fragment + }), + result = false; + try { + $.PrivateBin.Model.getPasteId(); + } + catch(err) { + result = true; + } + $.PrivateBin.Model.reset(); + clean(); + return result; + } + ); + }); + + describe('getPasteKey', function () { + this.timeout(30000); + jsc.property( + 'returns the fragment of the URL', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.nearray(common.jscBase64String()), + function (schema, address, query, fragment) { + var fragmentString = fragment.join(''), + clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/?' + query.join('') + '#' + fragmentString + }), + result = $.PrivateBin.Model.getPasteKey(); + $.PrivateBin.Model.reset(); + clean(); + return fragmentString === result; + } + ); + jsc.property( + 'returns the fragment stripped of trailing query parts', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.nearray(common.jscBase64String()), + jsc.array(common.jscQueryString()), + function (schema, address, query, fragment, trail) { + var fragmentString = fragment.join(''), + clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + '/?' + + query.join('') + '#' + fragmentString + '&' + trail.join('') + }), + result = $.PrivateBin.Model.getPasteKey(); + $.PrivateBin.Model.reset(); + clean(); + return fragmentString === result; + } + ); + jsc.property( + 'throws exception on empty fragment of the URL', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + function (schema, address, query) { + var clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/?' + query.join('') + }), + result = false; + try { + $.PrivateBin.Model.getPasteKey(); + } + catch(err) { + result = true; + } + $.PrivateBin.Model.reset(); + clean(); + return result; + } + ); + }); + + describe('getTemplate', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "[name]template"', + jsc.nearray(common.jscAlnumString()), + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscAlnumString()), + function (id, element, value) { + id = id.join(''); + element = element.join(''); + value = value.join('').trim(); + + //
,
, and tags can't contain strings, + // table tags can't be alone, so test with a

instead + if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) { + element = 'p'; + } + + $('body').html( + '

<' + element + ' id="' + id + + 'template">' + value + '
' + ); + $.PrivateBin.Model.init(); + var template = '<' + element + ' id="' + id + '">' + value + + '', + result = $.PrivateBin.Model.getTemplate(id).wrap('

').parent().html(); + $.PrivateBin.Model.reset(); + return template === result; + } + ); + }); +}); + diff --git a/js/test/PasteStatus.js b/js/test/PasteStatus.js new file mode 100644 index 0000000..6b0805d --- /dev/null +++ b/js/test/PasteStatus.js @@ -0,0 +1,107 @@ +'use strict'; +var common = require('../common'); + +describe('PasteStatus', function () { + describe('createPasteNotification', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'creates a notification after a successfull paste upload', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + 'string', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + function ( + schema1, address1, query1, fragment1, + schema2, address2, query2 + ) { + var expected1 = schema1 + '://' + address1.join('') + '/?' + + encodeURI(query1.join('').replace(/^&+|&+$/gm,'') + '#' + fragment1), + expected2 = schema2 + '://' + address2.join('') + '/?' + + encodeURI(query2.join('')), + clean = jsdom(); + $('body').html('

'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification(expected1, expected2); + var result1 = $('#pasteurl')[0].href, + result2 = $('#deletelink a')[0].href; + clean(); + return result1 === expected1 && result2 === expected2; + } + ); + }); + + describe('showRemainingTime', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'shows burn after reading message or remaining time', + 'bool', + 'nat', + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscA2zString()), + jsc.nearray(common.jscQueryString()), + 'string', + function ( + burnafterreading, remainingTime, + schema, address, query, fragment + ) { + var clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/?' + query.join('') + '#' + fragment + }), + result; + $('body').html(''); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.showRemainingTime({ + 'burnafterreading': burnafterreading, + 'remaining_time': remainingTime, + 'expire_date': remainingTime ? ((new Date()).getTime() / 1000) + remainingTime : 0 + }); + if (burnafterreading) { + result = $('#remainingtime').hasClass('foryoureyesonly') && + !$('#remainingtime').hasClass('hidden'); + } else if (remainingTime) { + result =!$('#remainingtime').hasClass('foryoureyesonly') && + !$('#remainingtime').hasClass('hidden'); + } else { + result = $('#remainingtime').hasClass('hidden') && + !$('#remainingtime').hasClass('foryoureyesonly'); + } + clean(); + return result; + } + ); + }); + + describe('hideMessages', function () { + before(function () { + cleanup(); + }); + + it( + 'hides all messages', + function() { + $('body').html( + '
' + ); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.hideMessages(); + assert.ok( + $('#remainingtime').hasClass('hidden') && + $('#pastesuccess').hasClass('hidden') + ); + } + ); + }); +}); + diff --git a/js/test/PasteViewer.js b/js/test/PasteViewer.js new file mode 100644 index 0000000..64e2120 --- /dev/null +++ b/js/test/PasteViewer.js @@ -0,0 +1,121 @@ +'use strict'; +var common = require('../common'); + +describe('PasteViewer', function () { + describe('run, hide, getText, setText, getFormat, setFormat & isPrettyPrinted', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'displays text according to format', + common.jscFormats(), + 'nestring', + function (format, text) { + var clean = jsdom(), + results = []; + $('body').html( + '' + ); + $.PrivateBin.PasteViewer.init(); + $.PrivateBin.PasteViewer.setFormat(format); + $.PrivateBin.PasteViewer.setText(''); + results.push( + $('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') && + $.PrivateBin.PasteViewer.getFormat() === format && + $.PrivateBin.PasteViewer.getText() === '' + ); + $.PrivateBin.PasteViewer.run(); + results.push( + !$('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + $.PrivateBin.PasteViewer.hide(); + results.push( + $('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + $.PrivateBin.PasteViewer.setText(text); + $.PrivateBin.PasteViewer.run(); + results.push( + $('#placeholder').hasClass('hidden') && + !$.PrivateBin.PasteViewer.isPrettyPrinted() && + $.PrivateBin.PasteViewer.getText() === text + ); + if (format === 'markdown') { + results.push( + $('#prettymessage').hasClass('hidden') && + !$('#plaintext').hasClass('hidden') + ); + } else { + results.push( + !$('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + } + clean(); + return results.every(element => element); + } + ); + + jsc.property( + 'sanitizes XSS', + common.jscFormats(), + 'string', + // @see {@link https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet} + jsc.elements([ + '', + '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>', + '\'\';!--"<XSS>=&{()}', + '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>', + '\'">><marquee><img src=x onerror=confirm(1)></marquee>">' + + '</plaintext\\></|\\><plaintext/onmouseover=prompt(1)>' + + '<script>prompt(1)</script>@gmail.com<isindex formaction=' + + 'javascript:alert(/XSS/) type=submit>\'-->"></script>' + + '<script>alert(document.cookie)</script>"><img/id="confirm' + + '&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">', + '<IMG SRC="javascript:alert(\'XSS\');">', + '<IMG SRC=javascript:alert(\'XSS\')>', + '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>', + '<IMG SRC=javascript:alert(&quot;XSS&quot;)>', + '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>', + '<a onmouseover="alert(document.cookie)">xxs link</a>', + '<a onmouseover=alert(document.cookie)>xxs link</a>', + '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">', + '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>', + '<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">', + '<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>', + '<TABLE BACKGROUND="javascript:alert(\'XSS\')">', + '<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">', + '<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="httx://xss.rocks/xss.js"></SCRIPT>' + ]), + 'string', + function (format, prefix, xss, suffix) { + var clean = jsdom(), + text = prefix + xss + suffix; + $('body').html( + '<div id="placeholder" class="hidden">+++ no paste text ' + + '+++</div><div id="prettymessage" class="hidden"><pre ' + + 'id="prettyprint" class="prettyprint linenums:1"></pre>' + + '</div><div id="plaintext" class="hidden"></div>' + ); + $.PrivateBin.PasteViewer.init(); + $.PrivateBin.PasteViewer.setFormat(format); + $.PrivateBin.PasteViewer.setText(text); + $.PrivateBin.PasteViewer.run(); + var result = $('body').html().indexOf(xss) === -1; + clean(); + return result; + } + ); + }); +}); + diff --git a/js/test/Prompt.js b/js/test/Prompt.js new file mode 100644 index 0000000..0e65b25 --- /dev/null +++ b/js/test/Prompt.js @@ -0,0 +1,42 @@ +'use strict'; +require('../common'); + +describe('Prompt', function () { + // TODO: this does not test the prompt() fallback, since that isn't available + // in nodejs -> replace the prompt in the "page" template with a modal + describe('requestPassword & getPassword', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the password fed into the dialog', + 'string', + function (password) { + password = password.replace(/\r+/g, ''); + var clean = jsdom('', {url: 'ftp://example.com/?0'}); + $('body').html( + '<div id="passwordmodal" class="modal fade" role="dialog">' + + '<div class="modal-dialog"><div class="modal-content">' + + '<div class="modal-body"><form id="passwordform" role="form">' + + '<div class="form-group"><input id="passworddecrypt" ' + + 'type="password" class="form-control" placeholder="Enter ' + + 'password"></div><button type="submit">Decrypt</button>' + + '</form></div></div></div></div>' + ); + $.PrivateBin.Model.init(); + $.PrivateBin.Prompt.init(); + $.PrivateBin.Prompt.requestPassword(); + $('#passworddecrypt').val(password); + $('#passwordform').submit(); + var result = $.PrivateBin.Prompt.getPassword(); + $.PrivateBin.Model.reset(); + clean(); + return result === password; + } + ); + }); +}); + diff --git a/js/test/TopNav.js b/js/test/TopNav.js new file mode 100644 index 0000000..e83d505 --- /dev/null +++ b/js/test/TopNav.js @@ -0,0 +1,548 @@ +'use strict'; +var common = require('../common'); + +describe('TopNav', function () { + describe('showViewButtons & hideViewButtons', function () { + before(function () { + cleanup(); + }); + + it( + 'displays & hides navigation elements for viewing an existing paste', + function () { + var results = []; + $('body').html( + '<nav class="navbar navbar-inverse navbar-static-top">' + + '<div id="navbar" class="navbar-collapse collapse"><ul ' + + 'class="nav navbar-nav"><li><button id="newbutton" ' + + 'type="button" class="hidden btn btn-warning navbar-btn">' + + '<span class="glyphicon glyphicon-file" aria-hidden="true">' + + '</span> New</button><button id="clonebutton" type="button"' + + ' class="hidden btn btn-warning navbar-btn">' + + '<span class="glyphicon glyphicon-duplicate" ' + + 'aria-hidden="true"></span> Clone</button><button ' + + 'id="rawtextbutton" type="button" class="hidden btn ' + + 'btn-warning navbar-btn"><span class="glyphicon ' + + 'glyphicon-text-background" aria-hidden="true"></span> ' + + 'Raw text</button><button id="qrcodelink" type="button" ' + + 'data-toggle="modal" data-target="#qrcodemodal" ' + + 'class="hidden btn btn-warning navbar-btn"><span ' + + 'class="glyphicon glyphicon-qrcode" aria-hidden="true">' + + '</span> QR code</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $('#newbutton').hasClass('hidden') && + $('#clonebutton').hasClass('hidden') && + $('#rawtextbutton').hasClass('hidden') && + $('#qrcodelink').hasClass('hidden') + ); + $.PrivateBin.TopNav.showViewButtons(); + results.push( + !$('#newbutton').hasClass('hidden') && + !$('#clonebutton').hasClass('hidden') && + !$('#rawtextbutton').hasClass('hidden') && + !$('#qrcodelink').hasClass('hidden') + ); + $.PrivateBin.TopNav.hideViewButtons(); + results.push( + $('#newbutton').hasClass('hidden') && + $('#clonebutton').hasClass('hidden') && + $('#rawtextbutton').hasClass('hidden') && + $('#qrcodelink').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('showCreateButtons & hideCreateButtons', function () { + before(function () { + cleanup(); + }); + + it( + 'displays & hides navigation elements for creating a paste', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li><button id="newbutton" ' + + 'type="button" class="hidden">New</button></li><li><a ' + + 'id="expiration" href="#" class="hidden">Expiration</a>' + + '</li><li><div id="burnafterreadingoption" class="hidden">' + + 'Burn after reading</div></li><li><div id="opendiscussion' + + 'option" class="hidden">Open discussion</div></li><li>' + + '<div id="password" class="hidden">Password</div></li>' + + '<li id="attach" class="hidden">Attach a file</li><li>' + + '<a id="formatter" href="#" class="hidden">Format</a>' + + '</li><li><button id="sendbutton" type="button" ' + + 'class="hidden">Send</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $('#sendbutton').hasClass('hidden') && + $('#expiration').hasClass('hidden') && + $('#formatter').hasClass('hidden') && + $('#burnafterreadingoption').hasClass('hidden') && + $('#opendiscussionoption').hasClass('hidden') && + $('#newbutton').hasClass('hidden') && + $('#password').hasClass('hidden') && + $('#attach').hasClass('hidden') + ); + $.PrivateBin.TopNav.showCreateButtons(); + results.push( + !$('#sendbutton').hasClass('hidden') && + !$('#expiration').hasClass('hidden') && + !$('#formatter').hasClass('hidden') && + !$('#burnafterreadingoption').hasClass('hidden') && + !$('#opendiscussionoption').hasClass('hidden') && + !$('#newbutton').hasClass('hidden') && + !$('#password').hasClass('hidden') && + !$('#attach').hasClass('hidden') + ); + $.PrivateBin.TopNav.hideCreateButtons(); + results.push( + $('#sendbutton').hasClass('hidden') && + $('#expiration').hasClass('hidden') && + $('#formatter').hasClass('hidden') && + $('#burnafterreadingoption').hasClass('hidden') && + $('#opendiscussionoption').hasClass('hidden') && + $('#newbutton').hasClass('hidden') && + $('#password').hasClass('hidden') && + $('#attach').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('showNewPasteButton', function () { + before(function () { + cleanup(); + }); + + it( + 'displays the button for creating a paste', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li><button id="newbutton" type=' + + '"button" class="hidden">New</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $('#newbutton').hasClass('hidden') + ); + $.PrivateBin.TopNav.showNewPasteButton(); + results.push( + !$('#newbutton').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('hideCloneButton', function () { + before(function () { + cleanup(); + }); + + it( + 'hides the button for cloning a paste', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li><button id="clonebutton" ' + + 'type="button" class="btn btn-warning navbar-btn">' + + '<span class="glyphicon glyphicon-duplicate" aria-hidden=' + + '"true"></span> Clone</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$('#clonebutton').hasClass('hidden') + ); + $.PrivateBin.TopNav.hideCloneButton(); + results.push( + $('#clonebutton').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('hideRawButton', function () { + before(function () { + cleanup(); + }); + + it( + 'hides the raw text button', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li><button ' + + 'id="rawtextbutton" type="button" class="btn ' + + 'btn-warning navbar-btn"><span class="glyphicon ' + + 'glyphicon-text-background" aria-hidden="true"></span> ' + + 'Raw text</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$('#rawtextbutton').hasClass('hidden') + ); + $.PrivateBin.TopNav.hideRawButton(); + results.push( + $('#rawtextbutton').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('hideFileSelector', function () { + before(function () { + cleanup(); + }); + + it( + 'hides the file attachment selection button', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="attach" class="hidden ' + + 'dropdown"><a href="#" class="dropdown-toggle" data-' + + 'toggle="dropdown" role="button" aria-haspopup="true" ' + + 'aria-expanded="false">Attach a file <span class="caret">' + + '</span></a><ul class="dropdown-menu"><li id="filewrap">' + + '<div><input type="file" id="file" name="file" /></div>' + + '</li><li id="customattachment" class="hidden"></li><li>' + + '<a id="fileremovebutton" href="#">Remove attachment</a>' + + '</li></ul></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$('#filewrap').hasClass('hidden') + ); + $.PrivateBin.TopNav.hideFileSelector(); + results.push( + $('#filewrap').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('showCustomAttachment', function () { + before(function () { + cleanup(); + }); + + it( + 'display the custom file attachment', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="attach" class="hidden ' + + 'dropdown"><a href="#" class="dropdown-toggle" data-' + + 'toggle="dropdown" role="button" aria-haspopup="true" ' + + 'aria-expanded="false">Attach a file <span class="caret">' + + '</span></a><ul class="dropdown-menu"><li id="filewrap">' + + '<div><input type="file" id="file" name="file" /></div>' + + '</li><li id="customattachment" class="hidden"></li><li>' + + '<a id="fileremovebutton" href="#">Remove attachment</a>' + + '</li></ul></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $('#customattachment').hasClass('hidden') + ); + $.PrivateBin.TopNav.showCustomAttachment(); + results.push( + !$('#customattachment').hasClass('hidden') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('collapseBar', function () { + before(function () { + cleanup(); + }); + + it( + 'collapses the navigation when displayed on a small screen', + function () { + var results = []; + $('body').html( + '<nav><div class="navbar-header"><button type="button" ' + + 'class="navbar-toggle collapsed" data-toggle="collapse" ' + + 'data-target="#navbar" aria-expanded="false" aria-controls' + + '="navbar">Toggle navigation</button><a class="reloadlink ' + + 'navbar-brand" href=""><img alt="PrivateBin" ' + + 'src="img/icon.svg" width="38" /></a></div><div ' + + 'id="navbar"><ul><li><button id="newbutton" type=' + + '"button" class="hidden">New</button></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $('.navbar-toggle').hasClass('collapsed') && + $('#navbar').attr('aria-expanded') != 'true' + ); + $.PrivateBin.TopNav.collapseBar(); + results.push( + $('.navbar-toggle').hasClass('collapsed') && + $('#navbar').attr('aria-expanded') != 'true' + ); + $('.navbar-toggle').click(); + results.push( + !$('.navbar-toggle').hasClass('collapsed') && + $('#navbar').attr('aria-expanded') == 'true' + ); + $.PrivateBin.TopNav.collapseBar(); + results.push( + $('.navbar-toggle').hasClass('collapsed') && + $('#navbar').attr('aria-expanded') == 'false' + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('getExpiration', function () { + before(function () { + cleanup(); + }); + + it( + 'returns the currently selected expiration date', + function () { + $.PrivateBin.TopNav.init(); + assert.ok($.PrivateBin.TopNav.getExpiration() === '1week'); + } + ); + }); + + describe('getFileList', function () { + before(function () { + cleanup(); + }); + + var File = window.File, + FileList = window.FileList, + path = require('path'), + mime = require('mime-types'); + + // mocking file input as per https://github.com/jsdom/jsdom/issues/1272 + function createFile(file_path) { + var file = fs.statSync(file_path), + lastModified = file.mtimeMs, + size = file.size; + + return new File( + [new fs.readFileSync(file_path)], + path.basename(file_path), + { + lastModified, + type: mime.lookup(file_path) || '', + } + ) + } + + function addFileList(input, file_paths) { + if (typeof file_paths === 'string') + file_paths = [file_paths] + else if (!Array.isArray(file_paths)) { + throw new Error('file_paths needs to be a file path string or an Array of file path strings') + } + + const file_list = file_paths.map(fp => createFile(fp)) + file_list.__proto__ = Object.create(FileList.prototype) + + Object.defineProperty(input, 'files', { + value: file_list, + writeable: false, + }) + + return input + } + + it( + 'returns the selected files', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="attach" class="hidden ' + + 'dropdown"><a href="#" class="dropdown-toggle" data-' + + 'toggle="dropdown" role="button" aria-haspopup="true" ' + + 'aria-expanded="false">Attach a file <span class="caret">' + + '</span></a><ul class="dropdown-menu"><li id="filewrap">' + + '<div><input type="file" id="file" name="file" /></div>' + + '</li><li id="customattachment" class="hidden"></li><li>' + + '<a id="fileremovebutton" href="#">Remove attachment</a>' + + '</li></ul></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $.PrivateBin.TopNav.getFileList() === null + ); + addFileList($('#file')[0], [ + '../img/logo.svg', + '../img/busy.gif' + ]); + var files = $.PrivateBin.TopNav.getFileList(); + results.push( + files[0].name === 'logo.svg' && + files[1].name === 'busy.gif' + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('getBurnAfterReading', function () { + before(function () { + cleanup(); + }); + + it( + 'returns if the burn-after-reading checkbox was ticked', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' + + 'class="hidden"><label><input type="checkbox" ' + + 'id="burnafterreading" name="burnafterreading" /> ' + + 'Burn after reading</label></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$.PrivateBin.TopNav.getBurnAfterReading() + ); + $('#burnafterreading').attr('checked', 'checked'); + results.push( + $.PrivateBin.TopNav.getBurnAfterReading() + ); + $('#burnafterreading').removeAttr('checked'); + results.push( + !$.PrivateBin.TopNav.getBurnAfterReading() + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + describe('getOpenDiscussion', function () { + before(function () { + cleanup(); + }); + + it( + 'returns if the open-discussion checkbox was ticked', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="opendiscussionoption" ' + + 'class="hidden"><label><input type="checkbox" ' + + 'id="opendiscussion" name="opendiscussion" /> ' + + 'Open discussion</label></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$.PrivateBin.TopNav.getOpenDiscussion() + ); + $('#opendiscussion').attr('checked', 'checked'); + results.push( + $.PrivateBin.TopNav.getOpenDiscussion() + ); + $('#opendiscussion').removeAttr('checked'); + results.push( + !$.PrivateBin.TopNav.getOpenDiscussion() + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); + + + describe('getPassword', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'returns the contents of the password input', + 'string', + function (password) { + password = password.replace(/\r+/g, ''); + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li><div id="password" ' + + 'class="navbar-form hidden"><input type="password" ' + + 'id="passwordinput" placeholder="Password (recommended)" ' + + 'class="form-control" size="23" /></div></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + $.PrivateBin.TopNav.getPassword() === '' + ); + $('#passwordinput').val(password); + results.push( + $.PrivateBin.TopNav.getPassword() === password + ); + $('#passwordinput').val(''); + results.push( + $.PrivateBin.TopNav.getPassword() === '' + ); + cleanup(); + return results.every(element => element); + } + ); + }); + + describe('getCustomAttachment', function () { + before(function () { + cleanup(); + }); + + it( + 'returns the custom attachment element', + function () { + var results = []; + $('body').html( + '<nav><div id="navbar"><ul><li id="attach" class="hidden ' + + 'dropdown"><a href="#" class="dropdown-toggle" data-' + + 'toggle="dropdown" role="button" aria-haspopup="true" ' + + 'aria-expanded="false">Attach a file <span class="caret">' + + '</span></a><ul class="dropdown-menu"><li id="filewrap">' + + '<div><input type="file" id="file" name="file" /></div>' + + '</li><li id="customattachment" class="hidden"></li><li>' + + '<a id="fileremovebutton" href="#">Remove attachment</a>' + + '</li></ul></li></ul></div></nav>' + ); + $.PrivateBin.TopNav.init(); + results.push( + !$.PrivateBin.TopNav.getCustomAttachment().hasClass('test') + ); + $('#customattachment').addClass('test'); + results.push( + $.PrivateBin.TopNav.getCustomAttachment().hasClass('test') + ); + cleanup(); + assert.ok(results.every(element => element)); + } + ); + }); +}); + diff --git a/js/test/UiHelper.js b/js/test/UiHelper.js new file mode 100644 index 0000000..e669e60 --- /dev/null +++ b/js/test/UiHelper.js @@ -0,0 +1,124 @@ +'use strict'; +var common = require('../common'); + +describe('UiHelper', function () { + // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet. + // for now we use a mock function to trigger the event + describe('historyChange', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + cleanup(); + }); + + jsc.property( + 'redirects to home, when the state is null', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + function (schema, address) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', {url: expected}); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.mockHistoryChange(); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + + jsc.property( + 'does not redirect to home, when a new paste is created', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.nearray(common.jscBase64String()), + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/?' + + query.join('') + '#' + fragment.join(''), + clean = jsdom('', {url: expected}); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.mockHistoryChange([ + {type: 'newpaste'}, '', expected + ]); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + }); + + describe('reloadHome', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'redirects to home', + common.jscSchemas(), + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.nearray(common.jscBase64String()), + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', { + url: expected + '?' + query.join('') + '#' + fragment.join('') + }); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.reloadHome(); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + }); + + describe('isVisible', function () { + // TODO As per https://github.com/tmpvar/jsdom/issues/1048 there is no layout support in jsdom, yet. + // once it is supported or a workaround is found, uncomment the section below + /* + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'detect visible elements', + jsc.nearray(common.jscAlnumString()), + jsc.nearray(common.jscA2zString()), + function (id, element) { + id = id.join(''); + element = element.join(''); + var clean = jsdom( + '<' + element + ' id="' + id + '"></' + element + '>' + ); + var result = $.PrivateBin.UiHelper.isVisible($('#' + id)); + clean(); + return result; + } + ); + */ + }); + + describe('scrollTo', function () { + // TODO Did not find a way to test that, see isVisible test above + }); +}); + diff --git a/lib/Configuration.php b/lib/Configuration.php index 4130a22..35c0960 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -7,13 +7,14 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; use Exception; use PDO; +use PrivateBin\Persistence\DataStore; /** * Configuration @@ -50,8 +51,9 @@ class Configuration 'languageselection' => false, 'languagedefault' => '', 'urlshortener' => '', + 'qrcode' => true, 'icon' => 'identicon', - 'cspheader' => 'default-src \'none\'; manifest-src \'self\'; connect-src *; script-src \'self\'; style-src \'self\'; font-src \'self\'; img-src \'self\' data:; referrer no-referrer; sandbox allow-same-origin allow-scripts allow-forms allow-popups', + 'cspheader' => 'default-src \'none\'; manifest-src \'self\'; connect-src *; script-src \'self\'; style-src \'self\'; font-src \'self\'; img-src \'self\' data:; media-src data:; object-src data:; Referrer-Policy: \'no-referrer\'; sandbox allow-same-origin allow-scripts allow-forms allow-popups', 'zerobincompatibility' => false, ), 'expire' => array( @@ -99,7 +101,20 @@ class Configuration public function __construct() { $config = array(); - $configFile = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini'; + $configFile = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php'; + $configIni = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini'; + + // rename INI files to avoid configuration leakage + if (is_readable($configIni)) { + DataStore::prependRename($configIni, $configFile, ';'); + + // cleanup sample, too + $configIniSample = $configIni . '.sample'; + if (is_readable($configIniSample)) { + DataStore::prependRename($configIniSample, PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.sample.php', ';'); + } + } + if (is_readable($configFile)) { $config = parse_ini_file($configFile, true); foreach (array('main', 'model', 'model_options') as $section) { @@ -108,6 +123,7 @@ class Configuration } } } + $opts = '_options'; foreach (self::getDefaults() as $section => $values) { // fill missing sections with default values diff --git a/lib/Data/AbstractData.php b/lib/Data/AbstractData.php index 41260f8..f4960f9 100644 --- a/lib/Data/AbstractData.php +++ b/lib/Data/AbstractData.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Data; diff --git a/lib/Data/Database.php b/lib/Data/Database.php index c35df3b..9685edd 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Data; @@ -693,9 +693,8 @@ class Database extends AbstractData 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . self::_sanitizeIdentifier('comment') . '(pasteid);' ); - // no break, continue with updates for 0.22 - case '0.22': - case '1.0': + // no break, continue with updates for 0.22 and later + default: self::_exec( 'UPDATE ' . self::_sanitizeIdentifier('config') . ' SET value = ? WHERE id = ?', diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index 4100e29..10012eb 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Data; @@ -57,7 +57,7 @@ class Filesystem extends AbstractData public function create($pasteid, $paste) { $storagedir = self::_dataid2path($pasteid); - $file = $storagedir . $pasteid; + $file = $storagedir . $pasteid . '.php'; if (is_file($file)) { return false; } @@ -79,9 +79,7 @@ class Filesystem extends AbstractData if (!$this->exists($pasteid)) { return false; } - $paste = json_decode( - file_get_contents(self::_dataid2path($pasteid) . $pasteid) - ); + $paste = DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php'); if (property_exists($paste->meta, 'attachment')) { $paste->attachment = $paste->meta->attachment; unset($paste->meta->attachment); @@ -104,8 +102,8 @@ class Filesystem extends AbstractData $pastedir = self::_dataid2path($pasteid); if (is_dir($pastedir)) { // Delete the paste itself. - if (is_file($pastedir . $pasteid)) { - unlink($pastedir . $pasteid); + if (is_file($pastedir . $pasteid . '.php')) { + unlink($pastedir . $pasteid . '.php'); } // Delete discussion if it exists. @@ -133,7 +131,26 @@ class Filesystem extends AbstractData */ public function exists($pasteid) { - return is_file(self::_dataid2path($pasteid) . $pasteid); + $basePath = self::_dataid2path($pasteid) . $pasteid; + $pastePath = $basePath . '.php'; + // convert to PHP protected files if needed + if (is_readable($basePath)) { + DataStore::prependRename($basePath, $pastePath); + + // convert comments, too + $discdir = self::_dataid2discussionpath($pasteid); + if (is_dir($discdir)) { + $dir = dir($discdir); + while (false !== ($filename = $dir->read())) { + if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) { + $commentFilename = $discdir . $filename . '.php'; + DataStore::prependRename($discdir . $filename, $commentFilename); + } + } + $dir->close(); + } + } + return is_readable($pastePath); } /** @@ -149,7 +166,7 @@ class Filesystem extends AbstractData public function createComment($pasteid, $parentid, $commentid, $comment) { $storagedir = self::_dataid2discussionpath($pasteid); - $file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid; + $file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php'; if (is_file($file)) { return false; } @@ -171,15 +188,14 @@ class Filesystem extends AbstractData $comments = array(); $discdir = self::_dataid2discussionpath($pasteid); if (is_dir($discdir)) { - // Delete all files in discussion directory $dir = dir($discdir); while (false !== ($filename = $dir->read())) { - // Filename is in the form pasteid.commentid.parentid: + // Filename is in the form pasteid.commentid.parentid.php: // - pasteid is the paste this reply belongs to. // - commentid is the comment identifier itself. // - parentid is the comment this comment replies to (It can be pasteid) if (is_file($discdir . $filename)) { - $comment = json_decode(file_get_contents($discdir . $filename)); + $comment = DataStore::get($discdir . $filename); $items = explode('.', $filename); // Add some meta information not contained in file. $comment->id = $items[1]; @@ -211,7 +227,7 @@ class Filesystem extends AbstractData { return is_file( self::_dataid2discussionpath($pasteid) . - $pasteid . '.' . $commentid . '.' . $parentid + $pasteid . '.' . $commentid . '.' . $parentid . '.php' ); } @@ -253,7 +269,14 @@ class Filesystem extends AbstractData continue; } $thirdLevel = array_filter( - scandir($path), + array_map( + function ($filename) { + return strlen($filename) >= 20 ? + substr($filename, 0, -4) : + $filename; + }, + scandir($path) + ), 'PrivateBin\\Model\\Paste::isValidId' ); if (count($thirdLevel) == 0) { diff --git a/lib/Filter.php b/lib/Filter.php index 951e265..302c84c 100644 --- a/lib/Filter.php +++ b/lib/Filter.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; @@ -64,7 +64,7 @@ class Filter $i = 0; while (($size / 1024) >= 1) { $size = $size / 1024; - $i++; + ++$i; } return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . I18n::_($iec[$i]); } @@ -82,7 +82,7 @@ class Filter public static function slowEquals($a, $b) { $diff = strlen($a) ^ strlen($b); - for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + for ($i = 0; $i < strlen($a) && $i < strlen($b); ++$i) { $diff |= ord($a[$i]) ^ ord($b[$i]); } return $diff === 0; diff --git a/lib/I18n.php b/lib/I18n.php index 2bee73e..5ae9bad 100644 --- a/lib/I18n.php +++ b/lib/I18n.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; diff --git a/lib/Json.php b/lib/Json.php index 27993f9..ad96333 100644 --- a/lib/Json.php +++ b/lib/Json.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; diff --git a/lib/Model.php b/lib/Model.php index d1011f1..b4f084f 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; diff --git a/lib/Model/AbstractModel.php b/lib/Model/AbstractModel.php index 55956b7..0ac2317 100644 --- a/lib/Model/AbstractModel.php +++ b/lib/Model/AbstractModel.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Model; diff --git a/lib/Model/Comment.php b/lib/Model/Comment.php index b67742d..709cdee 100644 --- a/lib/Model/Comment.php +++ b/lib/Model/Comment.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Model; diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index 26f8575..d8749a9 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Model; @@ -48,6 +48,11 @@ class Paste extends AbstractModel $data->meta->remaining_time = $data->meta->expire_date - time(); } + // check if non-expired burn after reading paste needs to be deleted + if (property_exists($data->meta, 'burnafterreading') && $data->meta->burnafterreading) { + $this->delete(); + } + // set formatter for for the view. if (!property_exists($data->meta, 'formatter')) { // support < 0.21 syntax highlighting diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 64fb530..2e31622 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Persistence; diff --git a/lib/Persistence/DataStore.php b/lib/Persistence/DataStore.php index 56dde1a..7ab4af5 100644 --- a/lib/Persistence/DataStore.php +++ b/lib/Persistence/DataStore.php @@ -22,6 +22,13 @@ use PrivateBin\Json; */ class DataStore extends AbstractPersistence { + /** + * first line in file, to protect its contents + * + * @const string + */ + const PROTECTION_LINE = '<?php http_response_code(403); /*'; + /** * store the data * @@ -38,10 +45,45 @@ class DataStore extends AbstractPersistence $filename = substr($filename, strlen($path)); } try { - self::_store($filename, Json::encode($data)); + self::_store($filename, self::PROTECTION_LINE . PHP_EOL . Json::encode($data)); return true; } catch (Exception $e) { return false; } } + + /** + * get the data + * + * @access public + * @static + * @param string $filename + * @return stdClass|false $data + */ + public static function get($filename) + { + return json_decode(substr(file_get_contents($filename), strlen(self::PROTECTION_LINE . PHP_EOL))); + } + + /** + * rename a file, prepending the protection line at the beginning + * + * @access public + * @static + * @param string $srcFile + * @param string $destFile + * @param string $prefix (optional) + * @return void + */ + public static function prependRename($srcFile, $destFile, $prefix = '') + { + // don't overwrite already converted file + if (!is_readable($destFile)) { + $handle = fopen($srcFile, 'r', false, stream_context_create()); + file_put_contents($destFile, $prefix . self::PROTECTION_LINE . PHP_EOL); + file_put_contents($destFile, $handle, FILE_APPEND); + fclose($handle); + } + unlink($srcFile); + } } diff --git a/lib/Persistence/PurgeLimiter.php b/lib/Persistence/PurgeLimiter.php index 2eb0b52..c4affac 100644 --- a/lib/Persistence/PurgeLimiter.php +++ b/lib/Persistence/PurgeLimiter.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Persistence; diff --git a/lib/Persistence/ServerSalt.php b/lib/Persistence/ServerSalt.php index 129a099..a4d0686 100644 --- a/lib/Persistence/ServerSalt.php +++ b/lib/Persistence/ServerSalt.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Persistence; diff --git a/lib/Persistence/TrafficLimiter.php b/lib/Persistence/TrafficLimiter.php index 914450a..9f35e5d 100644 --- a/lib/Persistence/TrafficLimiter.php +++ b/lib/Persistence/TrafficLimiter.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin\Persistence; diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index cd7ff57..1b16434 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; @@ -28,7 +28,7 @@ class PrivateBin * * @const string */ - const VERSION = '1.1'; + const VERSION = '1.1.1'; /** * minimal required PHP version @@ -147,10 +147,7 @@ class PrivateBin ); break; case 'read': - // reading paste is disallowed in HTML display - if ($this->_request->isJsonApiCall()) { - $this->_read($this->_request->getParam('pasteid')); - } + $this->_read($this->_request->getParam('pasteid')); break; case 'jsonld': $this->_jsonld($this->_request->getParam('jsonld')); @@ -179,8 +176,7 @@ class PrivateBin $this->_conf = new Configuration; $this->_model = new Model($this->_conf); $this->_request = new Request; - $this->_urlBase = array_key_exists('REQUEST_URI', $_SERVER) ? - htmlspecialchars($_SERVER['REQUEST_URI']) : '/'; + $this->_urlBase = $this->_request->getRequestUri(); ServerSalt::setPath($this->_conf->getKey('dir', 'traffic')); // set default language @@ -370,12 +366,15 @@ class PrivateBin try { $paste = $this->_model->getPaste($dataid); if ($paste->exists()) { - $data = $paste->get(); - $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date'); - if (property_exists($data->meta, 'salt')) { - unset($data->meta->salt); + // reading paste is only possible via JSON call + if ($this->_request->isJsonApiCall()) { + $data = $paste->get(); + $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date'); + if (property_exists($data->meta, 'salt')) { + unset($data->meta->salt); + } + $this->_data = json_encode($data); } - $this->_data = json_encode($data); } else { $this->_error = self::GENERIC_ERROR; } @@ -429,7 +428,6 @@ class PrivateBin $page = new View; $page->assign('NAME', $this->_conf->getKey('name')); - $page->assign('CIPHERDATA', $this->_data); $page->assign('ERROR', I18n::_($this->_error)); $page->assign('STATUS', I18n::_($this->_status)); $page->assign('VERSION', self::VERSION); @@ -451,6 +449,7 @@ class PrivateBin $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire')); $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire'))); $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener')); + $page->assign('QRCODE', $this->_conf->getKey('qrcode')); $page->draw($this->_conf->getKey('template')); } diff --git a/lib/Request.php b/lib/Request.php index 37c0bca..3fb35e3 100644 --- a/lib/Request.php +++ b/lib/Request.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; @@ -141,7 +141,20 @@ class Request */ public function getParam($param, $default = '') { - return array_key_exists($param, $this->_params) ? $this->_params[$param] : $default; + return array_key_exists($param, $this->_params) ? + $this->_params[$param] : $default; + } + + /** + * Get request URI + * + * @access public + * @return string + */ + public function getRequestUri() + { + return array_key_exists('REQUEST_URI', $_SERVER) ? + htmlspecialchars($_SERVER['REQUEST_URI']) : '/'; } /** diff --git a/lib/Sjcl.php b/lib/Sjcl.php index 4ed76b4..7efc7b2 100644 --- a/lib/Sjcl.php +++ b/lib/Sjcl.php @@ -7,7 +7,7 @@ * @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 + * @version 1.1.1 */ namespace PrivateBin; diff --git a/lib/View.php b/lib/View.php index 6c04e47..8b25395 100644 --- a/lib/View.php +++ b/lib/View.php @@ -7,7 +7,7 @@ * @link https://github.com/PrivateBin/PrivateBin * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License - * @version 1.1 + * @version 1.1.1 */ namespace PrivateBin; diff --git a/lib/Vizhash16x16.php b/lib/Vizhash16x16.php index e9bd5d0..3baae6d 100644 --- a/lib/Vizhash16x16.php +++ b/lib/Vizhash16x16.php @@ -8,7 +8,7 @@ * @link http://sebsauvage.net/wiki/doku.php?id=php:vizhash_gd * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License - * @version 0.0.5 beta PrivateBin 1.1 + * @version 0.0.5 beta PrivateBin 1.1.1 */ namespace PrivateBin; diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 8a09748..2616528 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -44,6 +44,11 @@ endif; <script type="text/javascript" src="js/jquery-3.1.1.js" integrity="sha512-U6K1YLIFUWcvuw5ucmMtT9HH4t0uz3M366qrF5y4vnyH6dgDzndlcGvH/Lz5k8NFh80SN95aJ5rqGZEdaQZ7ZQ==" crossorigin="anonymous"></script> <script type="text/javascript" src="js/sjcl-1.0.6.js" integrity="sha512-DsyxLV/uBoQlRTJmW5Gb2SxXUXB+aYeZ6zk+NuXy8LuLyi8oGti9AGn6He5fUY2DtgQ2//RjfaZog8exFuunUQ==" crossorigin="anonymous"></script> <?php +if ($QRCODE): +?> + <script async type="text/javascript" src="js/kjua-0.1.2.js" integrity="sha512-hmvfOhcr4J8bjQ2GuNVzfSbuulv72wgQCJpgnXc2+cCHKqvYo8pK2nc0Q4Esem2973zo1radyIMTEkt+xJlhBA==" crossorigin="anonymous"></script> +<?php +endif; if ($ZEROBINCOMPATIBILITY): ?> <script type="text/javascript" src="js/base64-1.7.js" integrity="sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==" crossorigin="anonymous"></script> @@ -66,10 +71,11 @@ endif; if ($MARKDOWN): ?> <script type="text/javascript" src="js/showdown-1.6.1.js" integrity="sha512-e6kAsBTgFnTBnEQXrq8BV6+XFwxb3kyWHeEPOl+KhxaWt3xImE2zAW2+yP3E2CQ7F9yoJl1poVU9qxkOEtVsTQ==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/purify-1.0.3.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-uhzhZJSgc+XJoaxCOjiuRzQaf5klPlSSVKGw69+zT72hhfLbVwB4jbwI+f7NRucuRz6u0aFGMeZ+0PnGh73iBQ==" crossorigin="anonymous"></script> <?php endif; ?> - <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-RFxFzaLFEWaKebOSTwwDtsfv1WBl7x/FOjBeoHxTGOeuwy5jUp2Woyiw4cJUtyMMlFHMQmd4REzomUIksszKKg==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-d0gyNw98fc/n9wRYLmGo2aO6HHWxApHHmWazdu/JJBppBrev9jcKRXJBRs65LtTADUHY/Whe247xvIBDHe7EpQ==" 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]--> @@ -87,8 +93,8 @@ if ($isCpct): ?> class="navbar-spacing"<?php endif; ?>> - <div id="passwordmodal" class="modal fade" role="dialog"> - <div class="modal-dialog"> + <div id="passwordmodal" tabindex="-1" class="modal fade" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-body"> <form id="passwordform" role="form"> @@ -102,6 +108,22 @@ endif; </div> </div> </div> +<?php +if ($QRCODE): +?> + <div id="qrcodemodal" tabindex="-1" class="modal fade" aria-labelledby="qrcodemodalTitle" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-body"> + <div class="mx-auto" id="qrcode-display"></div> + </div> + <button type="button" class="btn btn-primary btn-block" data-dismiss="modal"><?php echo I18n::_('Close') ?></button> + </div> + </div> + </div> +<?php +endif; +?> <nav class="navbar navbar-<?php echo $isDark ? 'inverse' : 'default'; ?> navbar-<?php echo $isCpct ? 'fixed' : 'static'; ?>-top"><?php if ($isCpct): ?><div class="container"><?php @@ -154,6 +176,15 @@ endif; <button id="rawtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn"> <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?> </button> +<?php +if ($QRCODE): +?> + <button id="qrcodelink" type="button" data-toggle="modal" data-target="#qrcodemodal" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn"> + <span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> <?php echo I18n::_('QR code'), PHP_EOL; ?> + </button> +<?php +endif; +?> </li> <li class="dropdown"> <select id="pasteExpiration" name="pasteExpiration" class="hidden"> @@ -235,6 +266,17 @@ if ($isCpct): ?> </ul> <select id="pasteFormatter" name="pasteFormatter" class="hidden"> +<?php + foreach ($FORMATTER as $key => $value): +?> + <option value="<?php echo $key; ?>"<?php + if ($key == $FORMATTERDEFAULT): +?> selected="selected"<?php + endif; +?>><?php echo $value; ?></option> +<?php + endforeach; +?> </select> </li> <?php @@ -264,7 +306,7 @@ else: endif; ?> /> <?php echo I18n::_('Open discussion'), PHP_EOL; ?> - </label> + </label> </div> </li> <?php @@ -274,7 +316,7 @@ if ($PASSWORD): ?> <li> <div id="password" class="navbar-form hidden"> - <input type="password" id="passwordinput" placeholder="<?php echo I18n::_('Password (recommended)'); ?>" class="form-control" size="19" /> + <input type="password" id="passwordinput" placeholder="<?php echo I18n::_('Password (recommended)'); ?>" class="form-control" size="23" /> </div> </li> <?php @@ -288,6 +330,7 @@ if ($FILEUPLOAD): <div> <input type="file" id="file" name="file" /> </div> + <div id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></div> </li> <li id="customattachment" class="hidden"></li> <li> @@ -428,20 +471,19 @@ endif; <a href="https://www.opera.com/">Opera</a>, <a href="https://www.google.com/chrome">Chrome</a>… </div> - <div id="pasteSuccess" role="alert" class="hidden alert alert-success"> + <div id="pastesuccess" role="alert" class="hidden alert alert-success"> <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <div id="deletelink"></div> - <div id="pastelink"> + <div id="pastelink"></div> <?php if (strlen($URLSHORTENER)): ?> - <button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?>"> - <span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?> - </button> + <button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?>"> + <span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?> + </button> <?php endif; ?> - </div> </div> <ul id="editorTabs" class="nav nav-tabs hidden"> <li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li> @@ -482,19 +524,18 @@ endif; </div> </footer> </main> - <div id="serverdata" class="hidden" aria-hidden="true"> <?php if ($DISCUSSION): ?> + <div id="serverdata" class="hidden" aria-hidden="true"> <div id="templates"> - <!-- @TODO: when I intend/structure this corrrectly Firefox adds whitespaces everywhere which completly destroy the layout. (same possible when you remove the template data below and show this area in the browser) --> <article id="commenttemplate" class="comment"><div class="commentmeta"><span class="nickname">name</span><span class="commentdate">0000-00-00</span></div><div class="commentdata">c</div><button class="btn btn-default btn-sm"><?php echo I18n::_('Reply'); ?></button></article> <p id="commenttailtemplate" class="comment"><button class="btn btn-default btn-sm"><?php echo I18n::_('Add comment'); ?></button></p> <div id="replytemplate" class="reply hidden"><input type="text" id="nickname" class="form-control" title="<?php echo I18n::_('Optional nickname…'); ?>" placeholder="<?php echo I18n::_('Optional nickname…'); ?>" /><textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea><br /><div id="replystatus" role="alert" class="statusmessage hidden alert"><span class="glyphicon" aria-hidden="true"></span> </div><button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button></div> </div> + </div> <?php endif; ?> - </div> </body> </html> diff --git a/tpl/page.php b/tpl/page.php index 172d7d4..b593f39 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -22,6 +22,7 @@ endif; ?> <script type="text/javascript" src="js/jquery-3.1.1.js" integrity="sha512-U6K1YLIFUWcvuw5ucmMtT9HH4t0uz3M366qrF5y4vnyH6dgDzndlcGvH/Lz5k8NFh80SN95aJ5rqGZEdaQZ7ZQ==" crossorigin="anonymous"></script> <script type="text/javascript" src="js/sjcl-1.0.6.js" integrity="sha512-DsyxLV/uBoQlRTJmW5Gb2SxXUXB+aYeZ6zk+NuXy8LuLyi8oGti9AGn6He5fUY2DtgQ2//RjfaZog8exFuunUQ==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/kjua.min.js" integrity="sha512-hmvfOhcr4J8bjQ2GuNVzfSbuulv72wgQCJpgnXc2+cCHKqvYo8pK2nc0Q4Esem2973zo1radyIMTEkt+xJlhBA==" crossorigin="anonymous"></script> <?php if ($ZEROBINCOMPATIBILITY): ?> @@ -44,10 +45,16 @@ endif; if ($MARKDOWN): ?> <script type="text/javascript" src="js/showdown-1.6.1.js" integrity="sha512-e6kAsBTgFnTBnEQXrq8BV6+XFwxb3kyWHeEPOl+KhxaWt3xImE2zAW2+yP3E2CQ7F9yoJl1poVU9qxkOEtVsTQ==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/purify-1.0.3.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-uhzhZJSgc+XJoaxCOjiuRzQaf5klPlSSVKGw69+zT72hhfLbVwB4jbwI+f7NRucuRz6u0aFGMeZ+0PnGh73iBQ==" crossorigin="anonymous"></script> +<?php +endif; +if ($QRCODE): +?> + <script async type="text/javascript" src="js/kjua-0.1.2.js" integrity="sha512-hmvfOhcr4J8bjQ2GuNVzfSbuulv72wgQCJpgnXc2+cCHKqvYo8pK2nc0Q4Esem2973zo1radyIMTEkt+xJlhBA==" crossorigin="anonymous"></script> <?php endif; ?> - <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-RFxFzaLFEWaKebOSTwwDtsfv1WBl7x/FOjBeoHxTGOeuwy5jUp2Woyiw4cJUtyMMlFHMQmd4REzomUIksszKKg==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-d0gyNw98fc/n9wRYLmGo2aO6HHWxApHHmWazdu/JJBppBrev9jcKRXJBRs65LtTADUHY/Whe247xvIBDHe7EpQ==" 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]--> @@ -99,6 +106,13 @@ if ($EXPIRECLONE): endif; ?> <button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" /><?php echo I18n::_('Raw text'); ?></button> +<?php +if ($QRCODE): +?> + <button id="qrcodelink" class="hidden"><img src="img/icon_qr.png" width="15" height="15" alt="" /><?php echo I18n::_('QR code'); ?></button> +<?php +endif; +?> <div id="expiration" class="hidden button"><?php echo I18n::_('Expires'); ?>: <select id="pasteExpiration" name="pasteExpiration"> <?php @@ -185,17 +199,22 @@ if (strlen($LANGUAGESELECTION)): endif; ?> </div> - <div id="pasteresult" class="hidden"> +<?php +if ($QRCODE): +?> + <div id="qrcode-display"></div> +<?php +endif; +?> <div id="pastesuccess" class="hidden"> <div id="deletelink"></div> - <div id="pastelink"> + <div id="pastelink"></div> <?php if (strlen($URLSHORTENER)): ?> - <button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>"><img src="img/icon_shorten.png" width="13" height="15" /><?php echo I18n::_('Shorten URL'); ?></button> + <button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>"><img src="img/icon_shorten.png" width="13" height="15" /><?php echo I18n::_('Shorten URL'); ?></button> <?php endif; ?> - </div> </div> <?php if ($FILEUPLOAD): @@ -204,6 +223,7 @@ if ($FILEUPLOAD): <div id="attach" class="hidden"> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span> <span id="filewrap"><?php echo I18n::_('Attach a file'); ?>: <input type="file" id="file" name="file" /></span> + <span id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></span> <button id="fileremovebutton"><?php echo I18n::_('Remove attachment'); ?></button> </div> <?php @@ -213,7 +233,7 @@ endif; <button id="messageedit"><?php echo I18n::_('Editor'); ?></button> <button id="messagepreview"><?php echo I18n::_('Preview'); ?></button> </div> - <div id="image" class="hidden"></div> + <div id="attachmentPreview" class="hidden"></div> <div id="prettymessage" class="hidden"> <pre id="prettyprint" class="prettyprint linenums:1"></pre> </div> @@ -227,21 +247,20 @@ endif; <div id="commentcontainer"></div> </div> </section> - <div id="serverdata" class="hidden" aria-hidden="true"> <?php if ($DISCUSSION): ?> + <div id="serverdata" class="hidden" aria-hidden="true"> <div id="templates"> - <!-- @TODO: when I intend/structure this corrrectly Firefox adds whitespaces everywhere which completly destroy the layout. (same possible when you remove the template data below and show this area in the browser) --> <article id="commenttemplate" class="comment"><div class="commentmeta"><span class="nickname">name</span><span class="commentdate">0000-00-00</span></div><div class="commentdata">c</div><button class="btn btn-default btn-sm"><?php echo I18n::_('Reply'); ?></button></article> <div id="commenttailtemplate" class="comment"><button class="btn btn-default btn-sm"><?php echo I18n::_('Add comment'); ?></button></div> <div id="replytemplate" class="reply hidden"><input type="text" id="nickname" class="form-control" title="<?php echo I18n::_('Optional nickname…'); ?>" placeholder="<?php echo I18n::_('Optional nickname…'); ?>" /><textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea><br /><div id="replystatus" role="alert" class="statusmessage hidden alert"><span class="glyphicon" aria-hidden="true"></span> </div><button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button></div> </div> + </div> <?php endif; ?> - </div> - <section class="container"> + <section class="container"> <div id="noscript" role="alert" class="nonworking alert alert-info noscript-hide"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"> <span> <?php echo I18n::_('Loading…'); ?></span><br> <span class="small"><?php echo I18n::_('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>.'); ?></span> diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php index dfae0ed..a954c40 100644 --- a/tst/Bootstrap.php +++ b/tst/Bootstrap.php @@ -12,10 +12,10 @@ if (!defined('PATH')) { define('PATH', '..' . DIRECTORY_SEPARATOR); } if (!defined('CONF')) { - define('CONF', PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini'); + define('CONF', PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php'); } -if (!is_file(CONF)) { - copy(CONF . '.sample', CONF); +if (!defined('CONF_SAMPLE')) { + define('CONF_SAMPLE', PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.sample.php'); } require PATH . 'vendor/autoload.php'; @@ -203,6 +203,9 @@ class Helper if (!is_file(CONF . '.bak') && is_file(CONF)) { rename(CONF, CONF . '.bak'); } + if (!is_file(CONF_SAMPLE . '.bak') && is_file(CONF_SAMPLE)) { + copy(CONF_SAMPLE, CONF_SAMPLE . '.bak'); + } } /** @@ -215,6 +218,9 @@ class Helper if (is_file(CONF . '.bak')) { rename(CONF . '.bak', CONF); } + if (is_file(CONF_SAMPLE . '.bak')) { + rename(CONF_SAMPLE . '.bak', CONF_SAMPLE); + } } /** diff --git a/tst/ConfigurationTest.php b/tst/ConfigurationTest.php index 3b9b442..15b7fd2 100644 --- a/tst/ConfigurationTest.php +++ b/tst/ConfigurationTest.php @@ -12,7 +12,7 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase { /* Setup Routine */ Helper::confBackup(); - $this->_options = configuration::getDefaults(); + $this->_options = Configuration::getDefaults(); $this->_options['model_options']['dir'] = PATH . $this->_options['model_options']['dir']; $this->_options['traffic']['dir'] = PATH . $this->_options['traffic']['dir']; $this->_options['purge']['dir'] = PATH . $this->_options['purge']['dir']; @@ -22,12 +22,14 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase public function tearDown() { /* Tear Down Routine */ + if (is_file(CONF)) { + unlink(CONF); + } Helper::confRestore(); } public function testDefaultConfigFile() { - $this->assertTrue(copy(CONF . '.bak', CONF), 'copy default configuration file'); $conf = new Configuration; $this->assertEquals($this->_options, $conf->get(), 'default configuration is correct'); } @@ -41,7 +43,9 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase public function testHandleMissingConfigFile() { - @unlink(CONF); + if (is_file(CONF)) { + unlink(CONF); + } $conf = new Configuration; $this->assertEquals($this->_options, $conf->get(), 'returns correct defaults on missing file'); } @@ -135,4 +139,42 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase $conf = new Configuration; $this->assertEquals('Database', $conf->getKey('class', 'model'), 'old db class gets renamed'); } + + public function testHandleConfigFileRename() + { + $options = $this->_options; + Helper::createIniFile(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini.sample', $options); + + $options['main']['opendiscussion'] = true; + $options['main']['fileupload'] = true; + $options['main']['template'] = 'darkstrap'; + Helper::createIniFile(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', $options); + + $conf = new Configuration; + $this->assertFileExists(CONF, 'old configuration file gets converted'); + $this->assertFileNotExists(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', 'old configuration file gets removed'); + $this->assertFileNotExists(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini.sample', 'old configuration sample file gets removed'); + $this->assertTrue( + $conf->getKey('opendiscussion') && + $conf->getKey('fileupload') && + $conf->getKey('template') === 'darkstrap', + 'configuration values get converted' + ); + } + + public function testRenameIniSample() + { + $iniSample = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini.sample'; + + Helper::createIniFile(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', $this->_options); + if (is_file(CONF)) { + unlink(CONF); + } + rename(CONF_SAMPLE, $iniSample); + new Configuration; + $this->assertFileNotExists($iniSample, 'old sample file gets removed'); + $this->assertFileExists(CONF_SAMPLE, 'new sample file gets created'); + $this->assertFileExists(CONF, 'old configuration file gets converted'); + $this->assertFileNotExists(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', 'old configuration file gets removed'); + } } diff --git a/tst/ConfigurationTestGenerator.php b/tst/ConfigurationTestGenerator.php index a011bed..aec2a73 100755 --- a/tst/ConfigurationTestGenerator.php +++ b/tst/ConfigurationTestGenerator.php @@ -159,7 +159,7 @@ new ConfigurationTestGenerator(array( array( 'type' => 'RegExp', 'args' => array( - '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d+\.\d+"[^>]*/>#', + '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#', '$content', 'outputs "page" stylesheet correctly', ), @@ -179,7 +179,7 @@ new ConfigurationTestGenerator(array( array( 'type' => 'NotRegExp', 'args' => array( - '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d+\.\d+"[^>]*/>#', + '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#', '$content', 'removes "page" stylesheet correctly', ), @@ -344,7 +344,7 @@ class ConfigurationTestGenerator */ private function _writeConfigurationTest() { - $defaultOptions = parse_ini_file(CONF, true); + $defaultOptions = parse_ini_file(CONF_SAMPLE, true); $code = $this->_getHeader(); foreach ($this->_configurations as $key => $conf) { $fullOptions = array_replace_recursive($defaultOptions, $conf['options']); @@ -425,7 +425,7 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase { /* Setup Routine */ Helper::confBackup(); - $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; + $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); ServerSalt::setPath($this->_path); TrafficLimiter::setPath($this->_path); @@ -435,9 +435,10 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase public function tearDown() { /* Tear Down Routine */ + unlink(CONF); Helper::confRestore(); Helper::rmDir($this->_path); -} + } public function reset($configuration = array()) { diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php index e7e6dc8..0a122f0 100644 --- a/tst/Data/FilesystemTest.php +++ b/tst/Data/FilesystemTest.php @@ -130,4 +130,49 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment'); $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist'); } + + public function testOldFilesGetConverted() + { + // generate 10 (default purge batch size) pastes in the old format + $paste = Helper::getPaste(); + $comment = Helper::getComment(); + $commentid = Helper::getCommentId(); + $ids = array(); + for ($i = 0, $max = 10; $i < $max; ++$i) { + // PHPs mt_rand only supports 32 bit or up 0x7fffffff on 64 bit systems to be precise :-/ + $dataid = str_pad(dechex(mt_rand(0, mt_getrandmax())), 8, '0', STR_PAD_LEFT) . + str_pad(dechex(mt_rand(0, mt_getrandmax())), 8, '0', STR_PAD_LEFT); + $storagedir = $this->_path . DIRECTORY_SEPARATOR . substr($dataid, 0, 2) . + DIRECTORY_SEPARATOR . substr($dataid, 2, 2) . DIRECTORY_SEPARATOR; + $ids[$dataid] = $storagedir; + + if (!is_dir($storagedir)) { + mkdir($storagedir, 0700, true); + } + file_put_contents($storagedir . $dataid, json_encode($paste)); + + $storagedir .= $dataid . '.discussion' . DIRECTORY_SEPARATOR; + if (!is_dir($storagedir)) { + mkdir($storagedir, 0700, true); + } + file_put_contents($storagedir . $dataid . '.' . $commentid . '.' . $dataid, json_encode($comment)); + } + // check that all 10 pastes were converted after the purge + $this->_model->purge(10); + foreach ($ids as $dataid => $storagedir) { + $this->assertFileExists($storagedir . $dataid . '.php', "paste $dataid exists in new format"); + $this->assertFileNotExists($storagedir . $dataid, "old format paste $dataid got removed"); + $this->assertTrue($this->_model->exists($dataid), "paste $dataid exists"); + $this->assertEquals($this->_model->read($dataid), json_decode(json_encode($paste)), "paste $dataid wasn't modified in the conversion"); + + $storagedir .= $dataid . '.discussion' . DIRECTORY_SEPARATOR; + $this->assertFileExists($storagedir . $dataid . '.' . $commentid . '.' . $dataid . '.php', "comment of $dataid exists in new format"); + $this->assertFileNotExists($storagedir . $dataid . '.' . $commentid . '.' . $dataid, "old format comment of $dataid got removed"); + $this->assertTrue($this->_model->existsComment($dataid, $dataid, $commentid), "comment in paste $dataid exists"); + $comment = json_decode(json_encode($comment)); + $comment->id = $commentid; + $comment->parentid = $dataid; + $this->assertEquals($this->_model->readComments($dataid), array($comment->meta->postdate => $comment), "comment of $dataid wasn't modified in the conversion"); + } + } } diff --git a/tst/I18nTest.php b/tst/I18nTest.php index c7ded0e..de16349 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -150,8 +150,8 @@ class I18nTest extends PHPUnit_Framework_TestCase $dir = dir(PATH . 'i18n'); while (false !== ($file = $dir->read())) { if (strlen($file) === 7) { - $language = substr($file, 0, 2); - $languageMessageIds = array_keys( + $language = substr($file, 0, 2); + $languageMessageIds = array_keys( json_decode( file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file), true diff --git a/tst/JsonApiTest.php b/tst/JsonApiTest.php index a592889..8588aca 100644 --- a/tst/JsonApiTest.php +++ b/tst/JsonApiTest.php @@ -14,30 +14,17 @@ class JsonApiTest extends PHPUnit_Framework_TestCase public function setUp() { /* Setup Routine */ - Helper::confBackup(); $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); ServerSalt::setPath($this->_path); - $this->reset(); - } - public function tearDown() - { - /* Tear Down Routine */ - Helper::confRestore(); - Helper::rmDir($this->_path); - } - - public function reset() - { $_POST = array(); $_GET = array(); $_SERVER = array(); if ($this->_model->exists(Helper::getPasteId())) { $this->_model->delete(Helper::getPasteId()); } - Helper::confRestore(); - $options = parse_ini_file(CONF, true); + $options = parse_ini_file(CONF_SAMPLE, true); $options['purge']['dir'] = $this->_path; $options['traffic']['dir'] = $this->_path; $options['model_options']['dir'] = $this->_path; @@ -45,15 +32,21 @@ class JsonApiTest extends PHPUnit_Framework_TestCase Helper::createIniFile(CONF, $options); } + public function tearDown() + { + /* Tear Down Routine */ + unlink(CONF); + Helper::confRestore(); + Helper::rmDir($this->_path); + } + /** * @runInSeparateProcess */ public function testCreate() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; @@ -80,10 +73,8 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testPut() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $paste = Helper::getPaste(); unset($paste['meta']); @@ -117,7 +108,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testDelete() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); $paste = $this->_model->read(Helper::getPasteId()); @@ -144,7 +134,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testDeleteWithPost() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); $paste = $this->_model->read(Helper::getPasteId()); @@ -168,7 +157,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testRead() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $paste['meta']['attachment'] = $paste['attachment']; $paste['meta']['attachmentname'] = $paste['attachmentname']; @@ -200,7 +188,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testJsonLdPaste() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $this->_model->create(Helper::getPasteId(), $paste); $_GET['jsonld'] = 'paste'; @@ -220,7 +207,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testJsonLdComment() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $this->_model->create(Helper::getPasteId(), $paste); $_GET['jsonld'] = 'comment'; @@ -240,7 +226,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testJsonLdPasteMeta() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $this->_model->create(Helper::getPasteId(), $paste); $_GET['jsonld'] = 'pastemeta'; @@ -260,7 +245,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testJsonLdCommentMeta() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $this->_model->create(Helper::getPasteId(), $paste); $_GET['jsonld'] = 'commentmeta'; @@ -280,10 +264,9 @@ class JsonApiTest extends PHPUnit_Framework_TestCase */ public function testJsonLdInvalid() { - $this->reset(); $paste = Helper::getPasteWithAttachment(); $this->_model->create(Helper::getPasteId(), $paste); - $_GET['jsonld'] = '../cfg/conf.ini'; + $_GET['jsonld'] = CONF; ob_start(); new PrivateBin; $content = ob_get_contents(); diff --git a/tst/ModelTest.php b/tst/ModelTest.php index 4d314f7..a41ed00 100644 --- a/tst/ModelTest.php +++ b/tst/ModelTest.php @@ -20,13 +20,12 @@ class ModelTest extends PHPUnit_Framework_TestCase public function setUp() { /* Setup Routine */ - Helper::confRestore(); $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; if (!is_dir($this->_path)) { mkdir($this->_path); } ServerSalt::setPath($this->_path); - $options = parse_ini_file(CONF, true); + $options = parse_ini_file(CONF_SAMPLE, true); $options['purge']['limit'] = 0; $options['model'] = array( 'class' => 'Database', @@ -47,6 +46,7 @@ class ModelTest extends PHPUnit_Framework_TestCase public function tearDown() { /* Tear Down Routine */ + unlink(CONF); Helper::confRestore(); Helper::rmDir($this->_path); } @@ -327,7 +327,6 @@ class ModelTest extends PHPUnit_Framework_TestCase 'pwd' => null, 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), ); - Helper::confBackup(); Helper::createIniFile(CONF, $options); $model = new Model(new Configuration); @@ -382,7 +381,6 @@ class ModelTest extends PHPUnit_Framework_TestCase 'pwd' => null, 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), ); - Helper::confBackup(); Helper::createIniFile(CONF, $options); $model = new Model(new Configuration); @@ -420,7 +418,6 @@ class ModelTest extends PHPUnit_Framework_TestCase 'pwd' => null, 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), ); - Helper::confBackup(); Helper::createIniFile(CONF, $options); $model = new Model(new Configuration); diff --git a/tst/PrivateBinTest.php b/tst/PrivateBinTest.php index a8aad11..c10f13a 100644 --- a/tst/PrivateBinTest.php +++ b/tst/PrivateBinTest.php @@ -16,13 +16,13 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase /* Setup Routine */ $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); - ServerSalt::setPath($this->_path); $this->reset(); } public function tearDown() { /* Tear Down Routine */ + unlink(CONF); Helper::confRestore(); Helper::rmDir($this->_path); } @@ -35,13 +35,12 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase if ($this->_model->exists(Helper::getPasteId())) { $this->_model->delete(Helper::getPasteId()); } - Helper::confRestore(); - $options = parse_ini_file(CONF, true); + $options = parse_ini_file(CONF_SAMPLE, true); $options['purge']['dir'] = $this->_path; $options['traffic']['dir'] = $this->_path; $options['model_options']['dir'] = $this->_path; - Helper::confBackup(); Helper::createIniFile(CONF, $options); + ServerSalt::setPath($this->_path); } /** @@ -49,7 +48,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testView() { - $this->reset(); ob_start(); new PrivateBin; $content = ob_get_contents(); @@ -71,10 +69,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testViewLanguageSelection() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['main']['languageselection'] = true; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_COOKIE['lang'] = 'de'; ob_start(); @@ -93,11 +89,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testViewForceLanguageDefault() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['main']['languageselection'] = false; $options['main']['languagedefault'] = 'fr'; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_COOKIE['lang'] = 'de'; ob_start(); @@ -116,11 +110,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testViewUrlShortener() { - $shortener = 'https://shortener.example.com/api?link='; - $this->reset(); + $shortener = 'https://shortener.example.com/api?link='; $options = parse_ini_file(CONF, true); $options['main']['urlshortener'] = $shortener; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_COOKIE['lang'] = 'de'; ob_start(); @@ -139,7 +131,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testHtaccess() { - $this->reset(); $file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess'; @unlink($file); @@ -160,8 +151,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testConf() { - $this->reset(); - Helper::confBackup(); file_put_contents(CONF, ''); new PrivateBin; } @@ -171,10 +160,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreate() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; @@ -200,10 +187,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidTimelimit() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(array('expire' => 25)); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; @@ -230,11 +215,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidSize() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['main']['sizelimit'] = 10; $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; @@ -254,10 +237,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateProxyHeader() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['header'] = 'X_FORWARDED_FOR'; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_SERVER['HTTP_X_FORWARDED_FOR'] = '::2'; @@ -284,10 +265,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateDuplicateId() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $_POST = Helper::getPaste(); @@ -308,10 +287,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateValidExpire() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['expire'] = '5min'; @@ -341,10 +318,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateValidExpireWithDiscussion() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['expire'] = '5min'; @@ -375,10 +350,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidExpire() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['expire'] = 'foo'; @@ -405,10 +378,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidBurn() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['burnafterreading'] = 'neither 1 nor 0'; @@ -429,10 +400,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidOpenDiscussion() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['opendiscussion'] = 'neither 1 nor 0'; @@ -453,11 +422,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateAttachment() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; $options['main']['fileupload'] = true; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPasteWithAttachment(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; @@ -491,11 +458,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateBrokenAttachmentUpload() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; $options['main']['fileupload'] = true; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPasteWithAttachment(); unset($_POST['attachment']); @@ -517,7 +482,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateTooSoon() { - $this->reset(); $_POST = Helper::getPaste(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; $_SERVER['REQUEST_METHOD'] = 'POST'; @@ -540,10 +504,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateValidNick() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getPaste(); $_POST['nickname'] = Helper::getComment()['meta']['nickname']; @@ -570,10 +532,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidNick() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getCommentPost(); $_POST['pasteid'] = Helper::getPasteId(); @@ -597,10 +557,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateComment() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getCommentPost(); $_POST['pasteid'] = Helper::getPasteId(); @@ -623,10 +581,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateInvalidComment() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getCommentPost(); $_POST['pasteid'] = Helper::getPasteId(); @@ -649,10 +605,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateCommentDiscussionDisabled() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getCommentPost(); $_POST['pasteid'] = Helper::getPasteId(); @@ -676,10 +630,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateCommentInvalidPaste() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $_POST = Helper::getCommentPost(); $_POST['pasteid'] = Helper::getPasteId(); @@ -701,10 +653,8 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testCreateDuplicateComment() { - $this->reset(); $options = parse_ini_file(CONF, true); $options['traffic']['limit'] = 0; - Helper::confBackup(); Helper::createIniFile(CONF, $options); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()); @@ -724,33 +674,11 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'paste exists after posting data'); } - /** - * @runInSeparateProcess - */ - public function testRead() - { - $this->reset(); - $this->_model->create(Helper::getPasteId(), Helper::getPaste()); - $_SERVER['QUERY_STRING'] = Helper::getPasteId(); - ob_start(); - new PrivateBin; - $content = ob_get_contents(); - ob_end_clean(); - $this->assertRegExp( - '#<div id="cipherdata"[^>]*>' . - preg_quote(htmlspecialchars(Helper::getPasteAsJson(), ENT_NOQUOTES)) . - '</div>#', - $content, - 'outputs data correctly' - ); - } - /** * @runInSeparateProcess */ public function testReadInvalidId() { - $this->reset(); $_SERVER['QUERY_STRING'] = 'foo'; ob_start(); new PrivateBin; @@ -768,7 +696,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadNonexisting() { - $this->reset(); $_SERVER['QUERY_STRING'] = Helper::getPasteId(); ob_start(); new PrivateBin; @@ -786,7 +713,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadExpired() { - $this->reset(); $expiredPaste = Helper::getPaste(array('expire_date' => 1344803344)); $this->_model->create(Helper::getPasteId(), $expiredPaste); $_SERVER['QUERY_STRING'] = Helper::getPasteId(); @@ -806,22 +732,27 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadBurn() { - $this->reset(); - $burnPaste = Helper::getPaste(array('burnafterreading' => true)); - $this->_model->create(Helper::getPasteId(), $burnPaste); - $_SERVER['QUERY_STRING'] = Helper::getPasteId(); + $paste = Helper::getPaste(array('burnafterreading' => true)); + $this->_model->create(Helper::getPasteId(), $paste); + $_SERVER['QUERY_STRING'] = Helper::getPasteId(); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; ob_start(); new PrivateBin; $content = ob_get_contents(); ob_end_clean(); - unset($burnPaste['meta']['salt']); - $this->assertRegExp( - '#<div id="cipherdata"[^>]*>' . - preg_quote(htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)) . - '</div>#', - $content, - 'outputs data correctly' - ); + $response = json_decode($content, true); + $this->assertEquals(0, $response['status'], 'outputs success status'); + $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly'); + $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste'); + $this->assertEquals($paste['data'], $response['data'], 'outputs data correctly'); + $this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly'); + $this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly'); + $this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly'); + $this->assertEquals(1, $response['meta']['burnafterreading'], 'outputs burnafterreading correctly'); + $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly'); + $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly'); + // by default it will be deleted instantly after it is read + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste exists after reading'); } /** @@ -829,7 +760,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadJson() { - $this->reset(); $paste = Helper::getPaste(); $this->_model->create(Helper::getPasteId(), $paste); $_SERVER['QUERY_STRING'] = Helper::getPasteId(); @@ -855,7 +785,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadInvalidJson() { - $this->reset(); $_SERVER['QUERY_STRING'] = Helper::getPasteId(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; ob_start(); @@ -871,53 +800,29 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testReadOldSyntax() { - $this->reset(); - $oldPaste = Helper::getPaste(); - $meta = array( + $paste = Helper::getPaste(); + $paste['meta'] = array( 'syntaxcoloring' => true, - 'postdate' => $oldPaste['meta']['postdate'], - 'opendiscussion' => $oldPaste['meta']['opendiscussion'], + 'postdate' => $paste['meta']['postdate'], + 'opendiscussion' => $paste['meta']['opendiscussion'], ); - $oldPaste['meta'] = $meta; - $this->_model->create(Helper::getPasteId(), $oldPaste); - $_SERVER['QUERY_STRING'] = Helper::getPasteId(); + $this->_model->create(Helper::getPasteId(), $paste); + $_SERVER['QUERY_STRING'] = Helper::getPasteId(); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; ob_start(); new PrivateBin; $content = ob_get_contents(); ob_end_clean(); - $meta['formatter'] = 'syntaxhighlighting'; - $this->assertRegExp( - '#<div id="cipherdata"[^>]*>' . - preg_quote(htmlspecialchars(Helper::getPasteAsJson($meta), ENT_NOQUOTES)) . - '</div>#', - $content, - 'outputs data correctly' - ); - } - - /** - * @runInSeparateProcess - */ - public function testReadOldFormat() - { - $this->reset(); - $oldPaste = Helper::getPaste(); - unset($oldPaste['meta']['formatter']); - $this->_model->create(Helper::getPasteId(), $oldPaste); - $_SERVER['QUERY_STRING'] = Helper::getPasteId(); - ob_start(); - new PrivateBin; - $content = ob_get_contents(); - ob_end_clean(); - $oldPaste['meta']['formatter'] = 'plaintext'; - unset($oldPaste['meta']['salt']); - $this->assertRegExp( - '#<div id="cipherdata"[^>]*>' . - preg_quote(htmlspecialchars(Helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES)) . - '</div>#', - $content, - 'outputs data correctly' - ); + $response = json_decode($content, true); + $this->assertEquals(0, $response['status'], 'outputs success status'); + $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly'); + $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste'); + $this->assertEquals($paste['data'], $response['data'], 'outputs data correctly'); + $this->assertEquals('syntaxhighlighting', $response['meta']['formatter'], 'outputs format correctly'); + $this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly'); + $this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly'); + $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly'); + $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly'); } /** @@ -925,7 +830,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDelete() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); $paste = $this->_model->read(Helper::getPasteId()); @@ -948,7 +852,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteInvalidId() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $_GET['pasteid'] = 'foo'; $_GET['deletetoken'] = 'bar'; @@ -969,7 +872,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteInexistantId() { - $this->reset(); $_GET['pasteid'] = Helper::getPasteId(); $_GET['deletetoken'] = 'bar'; ob_start(); @@ -988,7 +890,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteInvalidToken() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $_GET['pasteid'] = Helper::getPasteId(); $_GET['deletetoken'] = 'bar'; @@ -1009,7 +910,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteBurnAfterReading() { - $this->reset(); $burnPaste = Helper::getPaste(array('burnafterreading' => true)); $this->_model->create(Helper::getPasteId(), $burnPaste); $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); @@ -1031,7 +931,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteInvalidBurnAfterReading() { - $this->reset(); $this->_model->create(Helper::getPasteId(), Helper::getPaste()); $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); $_POST['deletetoken'] = 'burnafterreading'; @@ -1052,7 +951,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteExpired() { - $this->reset(); $expiredPaste = Helper::getPaste(array('expire_date' => 1000)); $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not exist before being created'); $this->_model->create(Helper::getPasteId(), $expiredPaste); @@ -1076,7 +974,6 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase */ public function testDeleteMissingPerPasteSalt() { - $this->reset(); $paste = Helper::getPaste(); unset($paste['meta']['salt']); $this->_model->create(Helper::getPasteId(), $paste); diff --git a/tst/PrivateBinWithDbTest.php b/tst/PrivateBinWithDbTest.php index 2ed3846..25e6082 100644 --- a/tst/PrivateBinWithDbTest.php +++ b/tst/PrivateBinWithDbTest.php @@ -1,7 +1,6 @@ <?php use PrivateBin\Data\Database; -use PrivateBin\Persistence\ServerSalt; require_once 'PrivateBinTest.php'; @@ -23,7 +22,6 @@ class PrivateBinWithDbTest extends PrivateBinTest if (!is_dir($this->_path)) { mkdir($this->_path); } - ServerSalt::setPath($this->_path); $this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3'; $this->_model = Database::getInstance($this->_options); $this->reset(); @@ -37,10 +35,7 @@ class PrivateBinWithDbTest extends PrivateBinTest $options['model'] = array( 'class' => 'Database', ); - $options['purge']['dir'] = $this->_path; - $options['traffic']['dir'] = $this->_path; - $options['model_options'] = $this->_options; - Helper::confBackup(); + $options['model_options'] = $this->_options; Helper::createIniFile(CONF, $options); } } diff --git a/tst/README.md b/tst/README.md index e11bc49..e6f7d9e 100644 --- a/tst/README.md +++ b/tst/README.md @@ -10,7 +10,7 @@ and their dependencies: Example for Debian and Ubuntu: ```console -$ sudo apt install phpunit php-gd php-sqlite php-xdebug +$ sudo apt install phpunit php-gd php-sqlite3 php-xdebug ``` To run the tests, change into the `tst` directory and run phpunit: @@ -51,7 +51,7 @@ and jsdom-global locally: ```console $ npm install -g mocha istanbul $ cd PrivateBin/js -$ npm install jsverify jsdom jsdom-global +$ npm install jsverify jsdom@9 jsdom-global@2 ``` Example for Debian and Ubuntu, including steps to allow the current user to @@ -63,9 +63,12 @@ $ sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} $ ln -s /usr/bin/nodejs /usr/local/bin/node $ npm install -g mocha istanbul $ cd PrivateBin/js -$ npm install jsverify jsdom jsdom-global +$ npm install jsverify jsdom@9 jsdom-global@2 ``` +Note: If you use a distribution that provides nodeJS >= 6, then you can install +the latest jsdom and jsdom-global packages and don't need to use @9 and @2. + To run the tests, just change into the `js` directory and run istanbul: ```console $ cd PrivateBin/js diff --git a/tst/ViewTest.php b/tst/ViewTest.php index e2e014b..8ced646 100644 --- a/tst/ViewTest.php +++ b/tst/ViewTest.php @@ -34,7 +34,6 @@ class ViewTest extends PHPUnit_Framework_TestCase /* Setup Routine */ $page = new View; $page->assign('NAME', 'PrivateBinTest'); - $page->assign('CIPHERDATA', Helper::getPaste()['data']); $page->assign('ERROR', self::$error); $page->assign('STATUS', self::$status); $page->assign('VERSION', self::$version); @@ -56,6 +55,7 @@ class ViewTest extends PHPUnit_Framework_TestCase $page->assign('EXPIREDEFAULT', self::$expire_default); $page->assign('EXPIRECLONE', true); $page->assign('URLSHORTENER', ''); + $page->assign('QRCODE', true); $dir = dir(PATH . 'tpl'); while (false !== ($file = $dir->read())) { @@ -96,13 +96,6 @@ class ViewTest extends PHPUnit_Framework_TestCase public function testTemplateRendersCorrectly() { foreach ($this->_content as $template => $content) { - $this->assertRegExp( - '#<div[^>]+id="cipherdata"[^>]*>' . - preg_quote(htmlspecialchars(Helper::getPaste()['data'], ENT_NOQUOTES)) . - '</div>#', - $content, - $template . ': outputs data correctly' - ); $this->assertRegExp( '#<div[^>]+id="errormessage"[^>]*>.*' . self::$error . '#s', $content,