Explorar o código

Merge remote-tracking branch 'PrivateBin/master'

Stefano Martinelli %!s(int64=9) %!d(string=hai) anos
pai
achega
e4c30e8613
Modificáronse 100 ficheiros con 6062 adicións e 3405 borrados
  1. 18 0
      .dockerignore
  2. 2 2
      .editorconfig
  3. 13 5
      .eslintrc
  4. 7 0
      .gitattributes
  5. 4 3
      .github/ISSUE_TEMPLATE.md
  6. 0 7
      .github/PULL_REQUEST_TEMPLATE.md
  7. 7 3
      .gitignore
  8. 1 1
      .htaccess.disabled
  9. 3 3
      .php_cs
  10. 26 0
      .styleci.yml
  11. 1 1
      .travis.yml
  12. 15 0
      CHANGELOG.md
  13. 10 1
      CREDITS.md
  14. 17 0
      Dockerfile
  15. 17 5
      INSTALL.md
  16. 3 3
      README.md
  17. 3 3
      browserconfig.xml
  18. 0 1
      cfg/.gitignore
  19. 1 2
      cfg/.htaccess
  20. 7 3
      cfg/conf.ini.sample
  21. 5 4
      composer.json
  22. 11 1
      css/bootstrap/privatebin.css
  23. 15 0
      css/noscript.css
  24. 13 9
      css/privatebin.css
  25. 35 4
      doc/README.md
  26. 44 32
      i18n/de.json
  27. 151 0
      i18n/es.json
  28. 30 23
      i18n/fr.json
  29. 151 0
      i18n/it.json
  30. 151 0
      i18n/no.json
  31. 160 0
      i18n/oc.json
  32. 34 27
      i18n/pl.json
  33. 151 0
      i18n/pt.json
  34. 159 0
      i18n/ru.json
  35. 29 22
      i18n/sl.json
  36. 32 25
      i18n/zh.json
  37. 0 0
      img/android-chrome-192x192.png
  38. 0 0
      img/android-chrome-512x512.png
  39. 0 0
      img/apple-touch-icon.png
  40. 0 0
      img/favicon-16x16.png
  41. 0 0
      img/favicon-32x32.png
  42. 0 0
      img/favicon.ico
  43. 0 0
      img/mstile-144x144.png
  44. 0 0
      img/mstile-150x150.png
  45. 0 0
      img/mstile-310x150.png
  46. 0 0
      img/mstile-310x310.png
  47. 0 0
      img/mstile-70x70.png
  48. 0 0
      img/safari-pinned-tab.svg
  49. 1 1
      index.php
  50. 7 0
      js/.istanbul.yml
  51. 0 5
      js/bootstrap-3.3.5.js
  52. 5 0
      js/bootstrap-3.3.7.js
  53. 0 1
      js/jquery-1.11.3.js
  54. 1 0
      js/jquery-3.1.1.js
  55. 3625 1176
      js/privatebin.js
  56. 0 0
      js/showdown-1.4.1.js
  57. 0 0
      js/showdown-1.6.1.js
  58. 0 58
      js/sjcl-1.0.4.js
  59. 60 0
      js/sjcl-1.0.6.js
  60. 174 0
      js/test.js
  61. 1 2
      lib/.htaccess
  62. 32 32
      lib/Configuration.php
  63. 1 1
      lib/Data/AbstractData.php
  64. 5 4
      lib/Data/Database.php
  65. 2 2
      lib/Data/Filesystem.php
  66. 1 17
      lib/Filter.php
  67. 10 5
      lib/I18n.php
  68. 1 1
      lib/Json.php
  69. 1 2
      lib/Model.php
  70. 2 2
      lib/Model/AbstractModel.php
  71. 4 4
      lib/Model/Comment.php
  72. 3 4
      lib/Model/Paste.php
  73. 1 1
      lib/Persistence/AbstractPersistence.php
  74. 1 1
      lib/Persistence/PurgeLimiter.php
  75. 1 1
      lib/Persistence/ServerSalt.php
  76. 1 1
      lib/Persistence/TrafficLimiter.php
  77. 24 19
      lib/PrivateBin.php
  78. 1 8
      lib/Request.php
  79. 2 2
      lib/Sjcl.php
  80. 3 2
      lib/View.php
  81. 7 8
      lib/Vizhash16x16.php
  82. 2 2
      manifest.json
  83. 9 0
      tpl/.editorconfig
  84. 1 0
      tpl/.htaccess
  85. 0 343
      tpl/bootstrap-compact.php
  86. 0 337
      tpl/bootstrap-dark-page.php
  87. 0 337
      tpl/bootstrap-dark.php
  88. 0 337
      tpl/bootstrap-page.php
  89. 260 101
      tpl/bootstrap.php
  90. 40 20
      tpl/page.php
  91. 13 13
      tst/Bootstrap.php
  92. 16 16
      tst/ConfigurationTest.php
  93. 82 83
      tst/ConfigurationTestGenerator.php
  94. 34 25
      tst/Data/DatabaseTest.php
  95. 13 8
      tst/Data/FilesystemTest.php
  96. 0 8
      tst/FilterTest.php
  97. 74 11
      tst/I18nTest.php
  98. 23 24
      tst/JsonApiTest.php
  99. 31 29
      tst/ModelTest.php
  100. 161 161
      tst/PrivateBinTest.php

+ 18 - 0
.dockerignore

@@ -0,0 +1,18 @@
+# Documentation, might leak version number
+CHANGELOG.md
+LICENSE.md
+CREDITS.md
+INSTALL.md
+README.md
+doc/
+
+# Dotfiles, pointless
+.codeclimate.yml
+.csslintrc
+.editorconfig
+.eslint*
+.git*
+.php_cs
+.styleci.yml
+.travis.yml
+.github

+ 2 - 2
.editorconfig

@@ -38,6 +38,6 @@ indent_style = space
 indent_size = 2
 trim_trailing_whitespace = false
 
-[.travis.yml]
+[*.yml]
 indent_style = space
-indent_size = 4
+indent_size = 2

+ 13 - 5
.eslintrc

@@ -15,7 +15,9 @@ globals:
 # http://eslint.org/docs/rules/
 rules:
   # Possible Errors
-  comma-dangle: [2, never]
+  comma-dangle:
+    - error
+    - never
   no-cond-assign: 2
   no-console: 0
   no-constant-condition: 2
@@ -31,7 +33,9 @@ rules:
   no-extra-parens: 0
   no-extra-semi: 2
   no-func-assign: 2
-  no-inner-declarations: [2, functions]
+  no-inner-declarations:
+    - error
+    - functions
   no-invalid-regexp: 2
   no-irregular-whitespace: 2
   no-negated-in-lhs: 2
@@ -47,7 +51,9 @@ rules:
   # Best Practices
   accessor-pairs: 2
   block-scoped-var: 0
-  complexity: [2, 6]
+  complexity:
+    - error
+    - 20
   consistent-return: 0
   curly: 0
   default-case: 0
@@ -99,7 +105,7 @@ rules:
   no-with: 2
   radix: 2
   vars-on-top: 0
-  wrap-iife: 2
+  wrap-iife: 0
   yoda: 0
 
   # Strict
@@ -152,7 +158,9 @@ rules:
   max-len: 0
   max-nested-callbacks: 0
   max-params: 0
-  max-statements: [2, 30]
+  max-statements:
+    - error
+    - 60
   new-cap: 0
   new-parens: 0
   newline-after-var: 0

+ 7 - 0
.gitattributes

@@ -1,7 +1,12 @@
 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
 .editorconfig export-ignore
 .eslintignore export-ignore
 .eslintrc export-ignore
@@ -9,4 +14,6 @@ tst/ export-ignore
 .github export-ignore
 .gitignore export-ignore
 .php_cs export-ignore
+.styleci.yml export-ignore
 .travis.yml export-ignore
+Dockerfile export-ignore

+ 4 - 3
.github/ISSUE_TEMPLATE.md

@@ -1,5 +1,5 @@
 <!-- Please have a look at our FAQ before submitting an issue: https://github.com/PrivateBin/PrivateBin/wiki/FAQ -->
-<!-- This is a template for an bug report. If you would like to suggest a feature, feel free to delete the part below. -->
+<!-- This is a template for a bug report. If you would like to suggest a feature, feel free to delete the part below. -->
 
 ## Steps to reproduce
 <!-- Tell us how to reproduce the problem. -->
@@ -18,8 +18,6 @@ Here you can add screenshots. If the issue is e.g. a client-side issue (= an iss
 If you have access to the server log files, also copy them here.
 -->
 
-* I can reproduce this issue on <https://privatebin.net>: Yes / No
-
 ## Basic information
 
 <!-- If you use a public server enter the address of it here. -->
@@ -36,3 +34,6 @@ If you have access to the server log files, also copy them here.
 
 <!-- The version of PrivateBin, if you use an unstable version paste the commit hash or the GitHub link to the commit here (you can get it by running `git rev-parse HEAD`) -->
 **PrivateBin version:**
+
+* I can reproduce this issue on <https://privatebin.net>: Yes / No
+

+ 0 - 7
.github/PULL_REQUEST_TEMPLATE.md

@@ -1,12 +1,5 @@
 <!-- This is a template for your Pull Request. This are just some suggestions for you. You do not have to use all of them. -->
 
-<!-- **Attention:** Please tick this box to agree to release your code under the projects license. Otherwise your PR cannot be accepted. -->
-* [ ] I agree to release the changes in this PR under the Zlib/libpng license as shown in the  [LICENSE file](https://github.com/PrivateBin/PrivateBin/blob/master/LICENSE.md#zliblibpng-license-for-privatebin).
-
-<!-- Alternatively uncomment and tick this box to release your code under public domain:
-* [ ] To the extent possible under law, I have waived all copyright and related or neighboring rights to this PR and publish it as public domain.
--->
-
 <!-- If your PR fixes an issue, mention it here. You can also just copy the URL - GitHub will convert it for you.
 If this PR fixes several issues, please prepend each issue url/number with the word "fix"/"fixes" or "close"/"closes" as this automatically closes the issues you mentioned when the PR is merged.
 -->

+ 7 - 3
.gitignore

@@ -1,6 +1,7 @@
 # Ignore server files for safety
 .htaccess
 .htpasswd
+cfg/conf.ini
 
 # Ignore data/
 data/
@@ -9,6 +10,10 @@ data/
 doc/*
 !doc/*.md
 
+# Ignore developers composer status so it isn't accidentally checked in,
+# see https://github.com/PrivateBin/PrivateBin/issues/84
+composer.lock
+
 # Ignore vendor dir of Composer except PHP files
 vendor/*.*
 vendor/*/*.*
@@ -23,9 +28,8 @@ vendor/**/tests
 vendor/**/build_phar.php
 !vendor/**/*.php
 
-composer.lock
-
-# Ignore unit testing logs, api docs and eclipse project files
+# Ignore local node modules, unit testing logs, api docs and eclipse project files
+js/node_modules/
 tst/log/
 .settings
 .buildpath

+ 1 - 1
.htaccess.disabled

@@ -1,3 +1,3 @@
 RewriteEngine on
-RewriteCond %{HTTP_USER_AGENT} ^.*(bot|spider|crawl|https?://|WhatsApp|SkypeUriPreview) [NC]
+RewriteCond %{HTTP_USER_AGENT} ^.*(bot|spider|crawl|https?://|WhatsApp|SkypeUriPreview|facebookexternalhit) [NC]
 RewriteRule .* - [R=403,L]

+ 3 - 3
.php_cs

@@ -15,9 +15,9 @@ return Symfony\CS\Config\Config::create()
     ->fixers(['concat_with_spaces', 'long_array_syntax', 'standardize_not_equal',
               'operators_spaces', 'duplicate_semicolon',
               'remove_leading_slash_use', 'align_equals',
-              'single_array_no_trailing_comma', 'phpdoc_indent', 'phpdoc_scalar',
-              'phpdoc_to_comment', 'phpdoc_trim',
+              'single_array_no_trailing_comma', 'phpdoc_indent', 'phpdoc_order',
+              'phpdoc_scalar', 'phpdoc_to_comment', 'phpdoc_trim',
               'phpdoc_types', 'print_to_echo', 'self_accessor', 'single_quote',
-              'spaces_cast', 'ternary_spaces', 'phpdoc_order'])
+              'spaces_cast', 'ternary_spaces'])
     ->finder($finder)
 ;

+ 26 - 0
.styleci.yml

@@ -0,0 +1,26 @@
+preset: recommended
+
+risky: false
+
+enabled:
+  - no_empty_comment
+  - align_equals
+  - long_array_syntax
+  - concat_with_spaces
+
+disabled:
+  - blank_line_after_opening_tag
+  - blank_line_before_return
+  - concat_without_spaces
+  - declare_equal_normalize
+  - heredoc_to_nowdoc
+  - method_argument_space
+  - new_with_braces
+  - phpdoc_align
+  - phpdoc_no_access
+  - phpdoc_separation
+  - phpdoc_single_line_var_spacing
+  - phpdoc_summary
+  - short_array_syntax
+  - single_line_after_imports
+  - unalign_equals

+ 1 - 1
.travis.yml

@@ -9,7 +9,7 @@ before_script:
   - composer install -n
 
 script:
-  - cd tst && phpunit
+  - cd tst && ../vendor/bin/phpunit
 
 after_script:
   - cd ..

+ 15 - 0
CHANGELOG.md

@@ -1,5 +1,20 @@
 # PrivateBin version history
 
+  * **next (not yet released)**
+    * ADDED: Translations for Spanish, Occitan, Norwegian and Portuguese
+    * ADDED: Option in configuration to change the default "PrivateBin" title of the site
+    * 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 (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
+    * ADDED: Dockerfile for docker container creation
+    * CHANGED: Using modal dialog to request password input instead of native JS input window (#69)
+    * CHANGED: Suppressed referrer HTTP header sending when following links in a paste or comment (#96) and added additional HTTP headers for XSS mitigation (#91)
+    * CHANGED: Updated random_compat and jQuery libraries
+    * FIXED: XSS using JavaScript stored as markdown formatted paste, after clicking on Raw paste button (#137)
+    * FIXED: Automatic purging deleting non-expiring pastes, when using database store (#149)
   * **1.0 (2016-08-25)**
     * ADDED: Translations for Slowene and Chinese
     * ADDED: re-introduced (optional) URL shortener support, which was removed back in version 0.16 for privacy concerns

+ 10 - 1
CREDITS.md

@@ -2,7 +2,7 @@
 
 ## Active contributors
 
-Simon Rupf - current developer and maintainer
+Simon Rupf - current developer and maintainer  
 rugk - security review, doc improvment & various other stuff
 
 ## Past contributions
@@ -19,6 +19,9 @@ Sébastien Sauvage - original idea and main developer
 * Adam Fisher - old Favicons
 * rugk - new logo/icons
 * Sobak - PSR-4 and PSR-2 refactoring
+* Nathaniel Olsen - jQuery upgrade
+* Alexander Demenshin - modal password dialog
+* PunKeel - Dockerfile
 
 ## Translations
 * Hexalyse - French
@@ -27,3 +30,9 @@ Sébastien Sauvage - original idea and main developer
 * Draky50110 - French
 * Filip Muki Dobranić - Slovene
 * Jiawei Zhou - Chinese
+* Stefano Marty - Italian
+* R4SAS - Russian
+* Alfredo Fabián Altamirano Tena - Spanish
+* Quent-in - Occitan
+* idarlund - Norwegian
+* Tulio Leao - Portuguese

+ 17 - 0
Dockerfile

@@ -0,0 +1,17 @@
+FROM php:apache
+
+RUN apt-get update && apt-get install -y \
+        libfreetype6-dev \
+        libjpeg62-turbo-dev \
+        libpng12-dev \
+        wget \
+        zip \
+        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; \
+
+    # We enable Apache's mod_rewrite
+    a2enmod rewrite
+
+COPY . .

+ 17 - 5
INSTALL.md

@@ -3,8 +3,8 @@
 **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 advise you to check
-the options and adjust them as you see fit.
+instance. We try to provide a safe default configuration, but we advise you to
+check the options and adjust them as you see fit.
 
 ## Basic installation
 
@@ -20,7 +20,7 @@ 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
-- (optional) a database supported by [PDO](https://secure.php.net/manual/book.pdo.php)
+- 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
 - A web browser with javascript support
 
@@ -39,6 +39,17 @@ 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
 
 ### Changing the path
@@ -104,7 +115,8 @@ The table prefix option is called `tbl`.
 > know.
 
 For reference or if you want to create the table schema for yourself (replace
-`prefix_` with your own table prefix):
+`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,
@@ -134,4 +146,4 @@ For reference or if you want to create the table schema for yourself (replace
     CREATE TABLE prefix_config (
         id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id)
     );
-    INSERT INTO prefix_config VALUES('VERSION', '0.22');
+    INSERT INTO prefix_config VALUES('VERSION', '1.1');

+ 3 - 3
README.md

@@ -7,7 +7,7 @@
 [![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.0*
+*Current version: 1.1*
 
 **PrivateBin** is a minimalist, open source online pastebin where the server has
 zero knowledge of pasted data.
@@ -16,10 +16,10 @@ Data is encrypted/decrypted in the browser using 256bit AES in [Galois Counter m
 
 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 much more features than the
+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
-without loosing any data.
+without losing any data.
 
 ## What PrivateBin provides
 

+ 3 - 3
browserconfig.xml

@@ -2,9 +2,9 @@
 <browserconfig>
   <msapplication>
     <tile>
-      <square150x150logo src="mstile-150x150.png"/>
-      <square310x310logo src="mstile-310x310.png"/>
-      <wide310x150logo src="mstile-310x150.png"/>
+      <square150x150logo src="img/mstile-150x150.png"/>
+      <square310x310logo src="img/mstile-310x310.png"/>
+      <wide310x150logo src="img/mstile-310x150.png"/>
       <TileColor>#ffc40d</TileColor>
     </tile>
   </msapplication>

+ 0 - 1
cfg/.gitignore

@@ -1 +0,0 @@
-/conf.ini

+ 1 - 2
cfg/.htaccess

@@ -1,2 +1 @@
-Allow from none
-Deny from all
+Require all denied

+ 7 - 3
cfg/conf.ini.sample

@@ -3,6 +3,9 @@
 ; An explanation of each setting can be find online at https://github.com/PrivateBin/PrivateBin/wiki/Configuration.
 
 [main]
+; (optional) set a project name to be displayed on the website
+; name = "PrivateBin"
+
 ; enable or disable the discussion feature, defaults to true
 discussion = true
 
@@ -25,10 +28,10 @@ defaultformatter = "plaintext"
 ; (optional) set a syntax highlighting theme, as found in css/prettify/
 ; syntaxhighlightingtheme = "sons-of-obsidian"
 
-; size limit per paste or comment in bytes, defaults to 2 Mibibytes
+; size limit per paste or comment in bytes, defaults to 2 Mebibytes
 sizelimit = 2097152
 
-; template to include, default is "bootstrap" (tpl/bootstrap.html)
+; template to include, default is "bootstrap" (tpl/bootstrap.php)
 template = "bootstrap"
 
 ; (optional) notice to display
@@ -60,7 +63,8 @@ languageselection = false
 ; custom scripts from third-party domains to your templates, e.g. tracking
 ; scripts or run your site behind certain DDoS-protection services.
 ; Check the documentation at https://content-security-policy.com/
-; 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;"
+; Note: If you use a bootstrap theme, you can remove the allow-popups from the sandbox restrictions.
+; 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"
 
 ; stay compatible with PrivateBin Alpha 0.19, less secure
 ; if enabled will use base64.js version 1.7 instead of 2.1.9 and sha1 instead of

+ 5 - 4
composer.json

@@ -18,13 +18,14 @@
         }
     ],
     "require": {
-        "php": "^5.2.6 || ^7.0",
-        "paragonie/random_compat": "^2.0",
-        "yzalis/identicon": "^1.1"
+        "php": "^5.3.0 || ^7.0",
+        "paragonie/random_compat": "2.0.4",
+        "yzalis/identicon": "1.1.0"
     },
     "require-dev": {
         "codacy/coverage": "dev-master",
-        "codeclimate/php-test-reporter": "dev-master"
+        "codeclimate/php-test-reporter": "dev-master",
+        "phpunit/phpunit": "^4.6 || ^5.0"
     },
     "autoload": {
         "psr-4": {

+ 11 - 1
css/bootstrap/privatebin.css

@@ -6,7 +6,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.0
+ * @version   1.1
  */
 
 body {
@@ -17,6 +17,10 @@ body.navbar-spacing {
 	padding-top: 70px;
 }
 
+body.loading {
+	cursor: wait;
+}
+
 .buttondisabled {
 	opacity: 0.3;
 }
@@ -102,6 +106,12 @@ body.navbar-spacing {
 	border-left: 1px solid #ccc;
 	padding: 5px 0 5px 10px;
 	white-space: pre-wrap;
+	transition: background-color 0.75s ease-out;
+}
+
+.comment.highlight {
+	background-color: #ffdd86;
+	transition: background-color 0.2s ease-in;
 }
 
 footer h4 {

+ 15 - 0
css/noscript.css

@@ -0,0 +1,15 @@
+/**
+ * PrivateBin
+ *
+ * CSS file only loaded when no JavaScript available.
+ *
+ * @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.0
+ */
+
+/* When there is no script at all other */
+.noscript-hide {
+	display: none;
+}

+ 13 - 9
css/privatebin.css

@@ -1,12 +1,12 @@
 /**
  * PrivateBin
  *
- * a zero-knowledge paste bin
+ * Main CSS file.
  *
  * @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.0
+ * @version   1.1
  */
 
 /*  CSS Reset from YUI 3.4.1 (build 4118) - Copyright 2011 Yahoo! Inc. All rights reserved.
@@ -117,7 +117,7 @@ h3.title {
 #pasteresult { 
 	background-color: #1F2833;
 	color: #fff;
-	padding: 4px 12px; 
+	padding: 4px 12px;
 	clear: both;
 	-moz-box-shadow: inset 0 2px 2px #000;
 	-webkit-box-shadow: inset 0 2px 2px #000;
@@ -161,7 +161,7 @@ button, .button {
 	background-clip: padding-box;
 }
 
-button:hover  { 
+button:hover  {
 	background-image: linear-gradient(bottom, #424b57 0%, #61707e 100%);
 	background-image: -o-linear-gradient(bottom, #424b57 0%, #61707e 100%);
 	background-image: -moz-linear-gradient(bottom, #424b57 0%, #61707e 100%);
@@ -193,8 +193,8 @@ button img {
 	top: 2px;
 }
 
-.button { 
-	background-color: #414d5a; 
+.button {
+	background-color: #414d5a;
 	padding: 6px 8px;
 	margin: 0 5px 0 0;
 	position: relative;
@@ -218,7 +218,7 @@ button img {
 }
 
 #remainingtime, #password {
-	color: #94a3b4; 
+	color: #94a3b4;
 	display: inline;
 	font-size: 0.85em;
 }
@@ -285,6 +285,10 @@ input {
 	color:#ff0;
 }
 
+.small {
+	font-size: 80%;
+}
+
 /* --- discussion related CSS ------- */
 
 #discussion {  /* Discussion container */
@@ -298,7 +302,7 @@ h4.title {
 	font-size: 1.2em;
 	color: #94a3b4;
 	font-style: italic;
-	font-weight: bold; 
+	font-weight: bold;
 	position: relative;
 	margin-left: 30px;
 }
@@ -356,7 +360,7 @@ h4.title {
 	background-clip: padding-box;
 }
 
-.comment button:hover  { 
+.comment button:hover  {
 	background-image: linear-gradient(bottom, #ccd 0, #fff 100%);
 	background-image: -o-linear-gradient(bottom, #ccd 0, #fff 100%);
 	background-image: -moz-linear-gradient(bottom, #ccd 0, #fff 100%);

+ 35 - 4
doc/README.md

@@ -1,5 +1,5 @@
-Generating documentation
-========================
+Generating PHP documentation
+============================
 
 In order to generate the documentation, you will need to install the following
 packages and its dependencies:
@@ -12,7 +12,7 @@ can be found in that projects documentation.
 
 Example for Debian and Ubuntu:
 ```console
-$ sudo aptitude install php-pear graphviz
+$ sudo apt install php-pear graphviz
 $ sudo pear channel-discover pear.phpdoc.org
 $ sudo pear install phpdoc/phpDocumentor
 ```
@@ -20,10 +20,41 @@ $ sudo pear install phpdoc/phpDocumentor
 To generate the documentation, change into the main directory and run phpdoc:
 ```console
 $ cd PrivateBin
-$ phpdoc -d lib/ -t doc/
+$ phpdoc -t doc/phpdoc -d lib/
 ```
 
 **Note:** When used with PHP 7, the prerelease of phpDocumentator 2.9 needs to be
 manually installed by downloading it from
 [GitHub](https://github.com/phpDocumentor/phpDocumentor2/releases/download/v2.9.0/phpDocumentor.phar)
 and then manually moving it to e.g. `/usr/local/bin` and making it executable.
+
+Generating JS documentation
+============================
+
+In order to generate the documentation, you will need to install the following
+packages and its dependencies:
+* npm
+
+Then you can use the node package manager to install the latest stable release
+of jsdoc globally:
+
+```console
+$ npm install -g jsdoc
+```
+
+Example for Debian and Ubuntu, including steps to allow current user to install
+node modules globally:
+```console
+$ sudo apt install npm
+$ sudo mkdir /usr/local/lib/node_modules
+$ sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}
+$ npm install -g jsdoc
+$ ln -s /usr/bin/nodejs /usr/local/bin/node
+```
+
+To generate the documentation, change into the main directory and run phpdoc:
+```console
+$ cd PrivateBin
+$ jsdoc -d doc/jsdoc js/privatebin.js
+```
+

+ 44 - 32
i18n/de.json

@@ -1,11 +1,16 @@
 {
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s ist ein minimalistischer, quelloffener \"Pastebin\"-artiger Dienst, bei dem der Server keinerlei Kenntnis der Inhalte hat. Die Daten werden <i>im Browser</i> mit 256 Bit AES ver- und entschlüsselt. Weitere Informationen sind auf der <a href=\"https://privatebin.info/\">Projektseite</a> zu finden.",
+    "Because ignorance is bliss":
+        "Unwissenheit ist ein Segen",
     "en": "de",
     "Paste does not exist, has expired or has been deleted.":
         "Diesen Text gibt es nicht, er ist abgelaufen oder wurde gelöscht.",
-    "PrivateBin requires php 5.3.0 or above to work. Sorry.":
-        "PrivateBin benötigt PHP 5.3.0 oder höher, um zu funktionieren. Sorry.",
-    "PrivateBin requires configuration section [%s] to be present in configuration file.":
-        "PrivateBin benötigt den Konfigurationsabschnitt [%s] in der Konfigurationsdatei um zu funktionieren.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s benötigt PHP 5.3.0 oder höher, um zu funktionieren. Sorry.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s benötigt den Konfigurationsabschnitt [%s] in der Konfigurationsdatei um zu funktionieren.",
     "Please wait %d seconds between each post.":
         "Bitte warte %d Sekunden zwischen dem Absenden.",
     "Paste is limited to %s of encrypted data.":
@@ -26,15 +31,10 @@
         "Falscher Lösch-Code. Text wurde nicht gelöscht.",
     "Paste was properly deleted.":
         "Text wurde erfolgreich gelöscht.",
-    "PrivateBin": "PrivateBin",
-    "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
-        "PrivateBin ist ein minimalistischer, quelloffener \"Pastebin\"-artiger Dienst, bei dem der Server keinerlei Kenntnis der Inhalte hat. Die Daten werden <i>im Browser</i> mit 256 Bit AES ver- und entschlüsselt. Weitere Informationen sind auf der <a href=\"https://privatebin.info/\">Projektseite</a> zu finden.",
-    "Because ignorance is bliss":
-        "Unwissenheit ist ein Segen",
-    "Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.":
-        "Javascript ist eine Voraussetzung, um PrivateBin zu nutzen.<br />Bitte entschuldige die Unannehmlichkeiten.",
-    "PrivateBin requires a modern browser to work.":
-        "PrivateBin setzt einen modernen Browser voraus, um funktionieren zu können.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "JavaScript ist eine Voraussetzung, um %s zu nutzen.<br />Bitte entschuldige die Unannehmlichkeiten.",
+    "%s requires a modern browser to work.":
+        "%s setzt einen modernen Browser voraus, um funktionieren zu können.",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
         "Du benutzt immer noch den Internet Explorer? Tu Dir einen Gefallen und wechsle zu einem moderneren Browser:",
     "New":
@@ -67,7 +67,7 @@
     "Never":
         "Nie",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
-        "Hinweis: Dies ist ein Versuchsdienst. Daten können jederzeit gelöscht werden. Kätzchen werden sterben wenn Du diesen Dienst missbrauchst.",
+        "Hinweis: Dies ist ein Versuchsdienst. Daten können jederzeit gelöscht werden. Kätzchen werden sterben wenn du diesen Dienst missbrauchst.",
     "This document will expire in %d seconds.":
         ["Dieses Dokument läuft in einer Sekunde ab.", "Dieses Dokument läuft in %d Sekunden ab."],
     "This document will expire in %d minutes.":
@@ -83,25 +83,25 @@
     "Could not decrypt data (Wrong key?)":
         "Konnte Daten nicht entschlüsseln (Falscher Schlüssel?)",
     "Could not delete the paste, it was not stored in burn after reading mode.":
-        "Konnte den Text nicht löschen, er wurde nicht im Einmal-Modus gespeichert.",
+        "Konnte das Paste nicht löschen, es wurde nicht im Einmal-Modus gespeichert.",
     "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
-        "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schliesse das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.",
+        "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schließe das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.",
     "Could not decrypt comment; Wrong key?":
         "Konnte Kommentar nicht entschlüsseln; Falscher Schlüssel?",
     "Reply":
         "Antworten",
     "Anonymous":
         "Anonym",
-    "Anonymous avatar (Vizhash of the IP address)":
-        "Anonymer Avatar (Vizhash der IP-Addresse)",
+    "Avatar generated from IP address":
+        "Avatar (generiert aus der IP-Adresse)",
     "Add comment":
         "Kommentar hinzufügen",
-    "Optional nickname...":
-        "Optionales Pseudonym...",
+    "Optional nickname":
+        "Optionales Pseudonym",
     "Post comment":
         "Kommentar absenden",
-    "Sending comment...":
-        "Sende Kommentar...",
+    "Sending comment":
+        "Sende Kommentar",
     "Comment posted.":
         "Kommentar gesendet.",
     "Could not refresh display: %s":
@@ -112,24 +112,25 @@
         "Fehler auf dem Server oder keine Antwort vom Server",
     "Could not post comment: %s":
         "Konnte Kommentar nicht senden: %s",
-    "Sending paste (Please move your mouse for more entropy)...":
-        "Sende Text (Bitte bewege Deine Maus um die Entropie zu erhöhen)...",
-    "Sending paste...":
-        "Sende Text...",
+    "Please move your mouse for more entropy…":
+        "Bitte bewege Deine Maus um die Entropie zu erhöhen",
+    "Sending paste":
+        "Sende Paste…",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
-        "Dein Text ist unter <a id=\"pasteurl\" href=\"%s\">%s</a> zu finden <span id=\"copyhint\">(Drücke [Strg]+[c] um den Link zu kopieren)</span>",
+        "Dein Paste ist unter <a id=\"pasteurl\" href=\"%s\">%s</a> zu finden <span id=\"copyhint\">(Drücke [Strg]+[c] um den Link zu kopieren)</span>",
     "Delete data":
         "Lösche Daten",
     "Could not create paste: %s":
-        "Konnte Text nicht erstellen: %s",
+        "Konnte Paste nicht erstellen: %s",
     "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
-        "Konnte Text nicht entschlüsseln: Der Schlüssel fehlt in der Adresse (Hast Du eine Umleitung oder einen URL-Verkürzer benutzt, der Teile der Adresse entfernt?)",
+        "Konnte Paste nicht entschlüsseln: Der Schlüssel fehlt in der Adresse (Hast du eine Umleitung oder einen URL-Verkürzer benutzt, der Teile der Adresse entfernt?)",
     "Format": "Format",
     "Plain Text": "Nur Text",
     "Source Code": "Quellcode",
     "Markdown": "Markdown",
     "Download attachment": "Anhang herunterladen",
-    "Cloned file attached.": "Kopierte Datei angehängt.",
+    "Cloned: '%s'": "Geklont: '%s'",
+    "The cloned file '%s' was attached to this paste.": "Die geklonte Datei '%s' wurde angehängt.",
     "Attach a file": "Datei anhängen",
     "Remove attachment": "Anhang entfernen",
     "Your browser does not support uploading encrypted files. Please use a newer browser.":
@@ -139,6 +140,17 @@
     "Shorten URL": "URL verkürzen",
     "Editor": "Bearbeiten",
     "Preview": "Vorschau",
-    "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
-        "Der PATH muss bei PrivateBin mit einem \"%s\" enden. Bitte passe Deinen PATH in Deiner index.php an."
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "Der PATH muss bei %s mit einem \"%s\" enden. Bitte passe Deinen PATH in Deiner index.php an.",
+    "Decrypt":
+        "Entschlüsseln",
+    "Enter password":
+        "Passwort eingeben",
+    "Loading…": "Lädt…",
+    "Decrypting paste…": "Entschlüssle Paste…",
+    "Preparing new paste…": "Bereite neues Paste vor…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">die FAQ</a> (englisch), um zu sehen, wie der Fehler behoben werden kann.",
+    "+++ no paste text +++":
+        "+++ kein Paste-Text +++"
 }

+ 151 - 0
i18n/es.json

@@ -0,0 +1,151 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s es un servicio de tipo \"Pastebin\" minimalista de código abierto, donde el servidor no tiene ningún conocimiento de los datos guardados. Los datos son cifrados/descifrados <i>en el navegador</i> usando 256 bits AES. Más información en la <a href=\"https://privatebin.info/\">página del proyecto</a>.",
+    "Because ignorance is bliss":
+        "Porque la ignorancia es dicha",
+    "en": "es",
+    "Paste does not exist, has expired or has been deleted.":
+        "El texto no existe, ha caducado o ha sido eliminado.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s requiere php 5.3.0 o superior para funcionar. Lo siento.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s requiere que la sección de configuración [%s] esté presente en el archivo de configuración.",
+    "Please wait %d seconds between each post.":
+        "Por favor espere %d segundos entre cada publicación.",
+    "Paste is limited to %s of encrypted data.":
+        "El texto está limitado a %s de datos cifrados.",
+    "Invalid data.":
+        "Datos inválidos.",
+    "You are unlucky. Try again.":
+        "Tienes mala suerte. Inténtalo de nuevo",
+    "Error saving comment. Sorry.":
+        "Error al guardar el comentario. Lo siento.",
+    "Error saving paste. Sorry.":
+        "Error al guardar el texto. Lo siento",
+    "Invalid paste ID.":
+        "ID del texto inválido.",
+    "Paste is not of burn-after-reading type.":
+        "El texto no es del tipo \"destruir despues de leer\".",
+    "Wrong deletion token. Paste was not deleted.":
+        "Token de eliminación erróneo. El texto no fue eliminado.",
+    "Paste was properly deleted.":
+        "El texto se ha eliminado correctamente.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "JavaScript es necesario para que %s funcione.<br />Sentimos los inconvenientes ocasionados.",
+    "%s requires a modern browser to work.":
+        "%s requiere un navegador moderno para funcionar.",
+    "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
+        "¿Sigues usando Internet Explorer? Hazte un favor, cambia a un navegador moderno:",
+    "New":
+        "Nuevo",
+    "Send":
+        "Enviar",
+    "Clone":
+        "Clonar",
+    "Raw text":
+        "Texto sin formato",
+    "Expires":
+        "Caducar en",
+    "Burn after reading":
+        "Destruir después de leer",
+    "Open discussion":
+        "Discusión abierta",
+    "Password (recommended)":
+        "Contraseña (recomendado)",
+    "Discussion":
+        "Discusión",
+    "Toggle navigation":
+        "Cambiar navegación",
+    "%d seconds": ["%d segundo", "%d segundos"],
+    "%d minutes": ["%d minuto", "%d minutos"],
+    "%d hours": ["%d hora", "%d horas"],
+    "%d days": ["%d día", "%d días"],
+    "%d weeks": ["%d semana", "%d semanas"],
+    "%d months": ["%d mes", "%d meses"],
+    "%d years": ["%d año", "%d años"],
+    "Never":
+        "Nunca",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
+        "Nota: Este es un servicio de prueba. Los datos pueden ser eliminados en cualquier momento. Gatitos morirán si se abusa de este servicio.",
+    "This document will expire in %d seconds.":
+        ["Este documento caducará en un segundo.", "Este documento caducará en %d segundos."],
+    "This document will expire in %d minutes.":
+        ["Este documento caducará en un minuto.", "Este documento caducará en %d minutos."],
+    "This document will expire in %d hours.":
+        ["Este documento caducará en una hora.", "Este documento caducará en %d horas."],
+    "This document will expire in %d days.":
+        ["Este documento caducará en un día.", "Este documento caducará en %d días."],
+    "This document will expire in %d months.":
+        ["Este documento caducará en un mes.", "Este documento caducará en %d meses."],
+    "Please enter the password for this paste:":
+        "Por favor ingrese la contraseña para este documento:",
+    "Could not decrypt data (Wrong key?)":
+        "No fue posible descifrar los datos (¿Clave errónea?)",
+    "Could not delete the paste, it was not stored in burn after reading mode.":
+        "No fue posible eliminar el documento, no fue guardado en modo \"destruir despues de leer\".",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
+        "SÓLO PARA TUS OJOS. No cierre esta ventana, este mensaje no se puede volver a mostrar.",
+    "Could not decrypt comment; Wrong key?":
+        "No se pudo descifrar el comentario; ¿Llave incorrecta?",
+    "Reply":
+        "Responder",
+    "Anonymous":
+        "Anónimo",
+    "Anonymous avatar (Vizhash of the IP address)":
+        "Avatar anónimo (Vizhash de la dirección IP)",
+    "Add comment":
+        "Añadir comentario",
+    "Optional nickname…":
+        "Seudónimo opcional…",
+    "Post comment":
+        "Publicar comentario",
+    "Sending comment…":
+        "Enviando comentario…",
+    "Comment posted.":
+        "Comentario publicado.",
+    "Could not refresh display: %s":
+        "No se pudo actualizar la vista: %s",
+    "unknown status":
+        "Estado desconocido",
+    "server error or not responding":
+        "Error del servidor o el servidor no responde",
+    "Could not post comment: %s":
+        "No fue posible publicar comentario: %s",
+    "Sending paste (Please move your mouse for more entropy)…":
+        "Enviando texto (Por favor, mueva el ratón para mayor entropía)…",
+    "Sending paste…":
+        "Enviando texto…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Su texto está en <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Presione [Ctrl]+[c] para copiar)</span>",
+    "Delete data":
+        "Eliminar datos",
+    "Could not create paste: %s":
+        "No fue posible crear el archivo: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
+        "No es posible descifrar el documento: Falta la clave de descifrado en la URL (¿Utilizó un redirector o un acortador de URL que quite parte de la URL?)",
+    "Format": "Formato",
+    "Plain Text": "Texto sin formato",
+    "Source Code": "Código fuente",
+    "Markdown": "Markdown",
+    "Download attachment": "Descargar adjunto",
+    "Cloned file attached.": "Archivo clonado adjunto.",
+    "Attach a file": "Adjuntar archivo",
+    "Remove attachment": "Remover adjunto",
+    "Your browser does not support uploading encrypted files. Please use a newer browser.":
+        "Tu navegador no admite la carga de archivos cifrados. Utilice un navegador más reciente.",
+    "Invalid attachment.": "Adjunto inválido.",
+    "Options": "Opciones",
+    "Shorten URL": "Acortar URL",
+    "Editor": "Editor",
+    "Preview": "Previsualización",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s requiere que el PATH termine en \"%s\". Por favor, actualice el PATH en su index.php.",
+    "Decrypt":
+        "Descifrar",
+    "Enter password":
+        "Ingrese contraseña",
+    "Loading…": "Cargando…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "En caso de que este mensaje nunca desaparezca por favor revise <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para obtener información para solucionar problemas</a>."
+}

+ 30 - 23
i18n/fr.json

@@ -1,11 +1,16 @@
 {
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%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 <i>dans le navigateur</i> par un chiffrage AES 256 bits. Plus d'informations sur <a href=\"https://privatebin.info/\">la page du projet</a>.",
+    "Because ignorance is bliss":
+        "Parce que l'ignorance c'est le bonheur",
     "en": "fr",
     "Paste does not exist, has expired or has been deleted.":
         "Le paste n'existe pas, a expiré, ou a été supprimé.",
-    "PrivateBin requires php 5.3.0 or above to work. Sorry.":
-        "Désolé, PrivateBin nécessite php 5.3.0 ou supérieur pour fonctionner.",
-    "PrivateBin requires configuration section [%s] to be present in configuration file.":
-        "PrivateBin a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "Désolé, %s nécessite php 5.3.0 ou supérieur pour fonctionner.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.",
     "Please wait %d seconds between each post.":
         "Merci d'attendre %d secondes entre chaque publication.",
     "Paste is limited to %s of encrypted data.":
@@ -26,15 +31,10 @@
         "Jeton de suppression incorrect. Le paste n'a pas été supprimé.",
     "Paste was properly deleted.":
         "Le paste a été correctement supprimé.",
-    "PrivateBin": "PrivateBin",
-    "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
-        "PrivateBin 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 <i>dans le navigateur</i> par un chiffrage AES 256 bits. Plus d'informations sur <a href=\"https://privatebin.info/\">la page du projet</a>.",
-    "Because ignorance is bliss":
-        "Parce que l'ignorance c'est le bonheur",
-    "Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.":
-        "Javascript est requis pour faire fonctionner PrivateBin. <br />Désolé pour cet inconvénient.",
-    "PrivateBin requires a modern browser to work.":
-        "PrivateBin nécessite un navigateur moderne pour fonctionner.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "JavaScript est requis pour faire fonctionner %s. <br />Désolé pour cet inconvénient.",
+    "%s requires a modern browser to work.":
+        "%s nécessite un navigateur moderne pour fonctionner.",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
         "Encore sur Internet Explorer ? Faites-vous une faveur, passez à un navigateur moderne :",
     "New":
@@ -96,12 +96,12 @@
         "Avatar anonyme (Vizhash de l'adresse IP)",
     "Add comment":
         "Ajouter un commentaire",
-    "Optional nickname...":
-        "Pseudonyme optionnel...",
+    "Optional nickname":
+        "Pseudonyme optionnel",
     "Post comment":
         "Poster le commentaire",
-    "Sending comment...":
-        "Envoi du commentaire...",
+    "Sending comment":
+        "Envoi du commentaire",
     "Comment posted.":
         "Commentaire posté.",
     "Could not refresh display: %s":
@@ -112,10 +112,10 @@
         "Le serveur ne répond pas ou a rencontré une erreur",
     "Could not post comment: %s":
         "Impossible de poster le commentaire : %s",
-    "Sending paste (Please move your mouse for more entropy)...":
-        "Envoi du paste (Merci de bouger votre souris pour plus d'entropie)...",
-    "Sending paste...":
-        "Envoi du paste...",
+    "Sending paste (Please move your mouse for more entropy)":
+        "Envoi du paste (Merci de bouger votre souris pour plus d'entropie)",
+    "Sending paste":
+        "Envoi du paste",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
         "Votre paste est disponible à l'adresse <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Appuyez sur [Ctrl]+[c] pour copier)</span>",
     "Delete data":
@@ -148,6 +148,13 @@
     "Shorten URL": "Raccourcir URL",
     "Editor": "Éditer",
     "Preview": "Prévisualiser",
-    "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
-        "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php."
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s requiert que le PATH se termine dans un \"%s\". Veuillez mettre à jour le PATH dans votre index.php.",
+    "Decrypt":
+        "Déchiffrer",
+    "Enter password":
+        "Entrez le mot de passe",
+    "Loading…": "Chargement…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Si ce message ne disparaîssait pas, jetez un oeil à <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">cette FAQ pour des idées de résolution</a> (en Anglais)."
 }

+ 151 - 0
i18n/it.json

@@ -0,0 +1,151 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s è un sistema di tipo \"Pastebin\" online, open source, minimalista. Il server non possiede alcuna conoscenza (\"Zero Knowledge\") del contenuto dei dati inviati. I dati sono cifrati/decifrati <i>nel Browser</i> con algoritmo AES a 256 Bit. Per ulteriori informazioni, vedi <a href=\"https://privatebin.info/\">Sito del progetto</a>.",
+    "Because ignorance is bliss":
+        "Perché l'ignoranza è una benedizione (Because ignorance is bliss)",
+    "en": "it",
+    "Paste does not exist, has expired or has been deleted.":
+        "Questo messaggio non esiste, è scaduto o è stato cancellato.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s richiede PHP 5.3.0 o superiore.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s richiede la presenza della sezione [%s] nei file di configurazione.",
+    "Please wait %d seconds between each post.":
+        "Attendi per favore %d secondi prima di ciascun invio.",
+    "Paste is limited to %s of encrypted data.":
+        "La dimensione del messaggio è limitata a %s di dati cifrati.",
+    "Invalid data.":
+        "Dati non validi.",
+    "You are unlucky. Try again.":
+        "Riprova, sarai più fortunato.",
+    "Error saving comment. Sorry.":
+        "Errore durante il salvataggio del commento.",
+    "Error saving paste. Sorry.":
+        "Errore durante il salvataggio del messaggio.",
+    "Invalid paste ID.":
+        "ID-Messaggio non valido.",
+    "Paste is not of burn-after-reading type.":
+        "Il messaggio non è di tipo Distruggi-dopo-lettura.",
+    "Wrong deletion token. Paste was not deleted.":
+        "Codice cancellazione errato. Il messaggio NON è stato cancellato.",
+    "Paste was properly deleted.":
+        "Il messaggio è stato correttamente cancellato.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "%s funziona solo con JavaScript attivo.<br />Ci dispiace per l'inconveniente.",
+    "%s requires a modern browser to work.":
+        "%s richiede un browser moderno e aggiornato per funzionare.",
+    "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
+        "Usi ancora Internet Explorer? Ti consigliamo di passare ad un browser più sicuro:",
+    "New":
+        "Nuovo",
+    "Send":
+        "Invia",
+    "Clone":
+        "Clona",
+    "Raw text":
+        "Testo Raw",
+    "Expires":
+        "Scade",
+    "Burn after reading":
+        "Distruggi dopo lettura",
+    "Open discussion":
+        "Apri discussione",
+    "Password (recommended)":
+        "Password (raccomandato)",
+    "Discussion":
+        "Discussione",
+    "Toggle navigation":
+        "Scambia Navigazione",
+    "%d seconds": ["%d secondo", "%d secondi"],
+    "%d minutes": ["%d minuto", "%d minuti"],
+    "%d hours": ["%d ora", "%d ore"],
+    "%d days": ["%d giorno", "%d giorni"],
+    "%d weeks": ["%d settimana", "%d settimane"],
+    "%d months": ["%d mese", "%d mesi"],
+    "%d years": ["%d anno", "%d anni"],
+    "Never":
+        "Mai",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
+        "Nota: questo è un servizio di prova, i dati possono essere cancellati in qualsiasi momento. Ti preghiamo di non abusare di questo servizio, grazie.",
+    "This document will expire in %d seconds.":
+        ["Questo documento scadrà tra un secondo.", "Questo documento scadrà in %d secondi."],
+    "This document will expire in %d minutes.":
+        ["Questo documento scadrà tra un minuto.", "Questo documento scadrà in %d minuti."],
+    "This document will expire in %d hours.":
+        ["Questo documento scadrà tra un'ora.", "Questo documento scadrà in %d ore."],
+    "This document will expire in %d days.":
+        ["Questo documento scadrà tra un giorno.", "Questo documento scadrà in %d giorni."],
+    "This document will expire in %d months.":
+        ["Questo documento scadrà tra un mese.", "Questo documento scadrà in %d mesi."],
+    "Please enter the password for this paste:":
+        "Inserisci la password per questo messaggio:",
+    "Could not decrypt data (Wrong key?)":
+        "Non riesco a decifrari i dati (Chiave errata?)",
+    "Could not delete the paste, it was not stored in burn after reading mode.":
+        "Non riesco a cancellare il messaggio, non è stato salvato in modalità Distruggi-dopo-lettora.",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
+        "FOR YOUR EYES ONLY. Non chiudere questa finestra, il messaggio non può essere visualizzato una seconda volta.",
+    "Could not decrypt comment; Wrong key?":
+        "Non riesco a decifrari il commento (Chiave errata?)",
+    "Reply":
+        "Rispondi",
+    "Anonymous":
+        "Anonimo",
+    "Anonymous avatar (Vizhash of the IP address)":
+        "Avatar Anonino (Vizhash dell'indirizzo IP)",
+    "Add comment":
+        "Aggiungi un commento",
+    "Optional nickname…":
+        "Nickname opzionale…",
+    "Post comment":
+        "Invia commento",
+    "Sending comment…":
+        "Commento in fase di invio…",
+    "Comment posted.":
+        "Commento inviato.",
+    "Could not refresh display: %s":
+        "Non riesco ad aggiornare il display: %s",
+    "unknown status":
+        "errore sconosciuto",
+    "server error or not responding":
+        "errore o mancata risposta dal server",
+    "Could not post comment: %s":
+        "Impossibile inviare il commento: %s",
+    "Sending paste (Please move your mouse for more entropy)…":
+        "Invio messaggio (Muovi il mouse in modo casuale, per generare maggior entropia)…",
+    "Sending paste…":
+        "Messaggio in fase di invio…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Il tuo messaggio è qui: <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">([CTRL | CMD]+[C] per copiare il link)</span>",
+    "Delete data":
+        "Cancella i dati",
+    "Could not create paste: %s":
+        "Non rieco a creare il messaggio: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
+        "Non riesco a decifrare il messaggio: manca la chiave di decifrazione nell'URL (La chiave è parte integrante dell'URL. Per caso hai usato un Redirector o un altro servizio che ha rimosso una parte dell'URL?)",
+    "Format": "Formato",
+    "Plain Text": "Solo Testo",
+    "Source Code": "Codice Sorgente",
+    "Markdown": "Markdown",
+    "Download attachment": "Scarica Allegato",
+    "Cloned file attached.": "Copia del file allegata.",
+    "Attach a file": "Allega un file",
+    "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.",
+    "Invalid attachment.": "Allegato non valido.",
+    "Options": "Opzioni",
+    "Shorten URL": "Accorcia URL",
+    "Editor": "Editor",
+    "Preview": "Preview",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s necessita che PATH termini con \"%s\". Aggiorna la variabile PATH nel tuo index.php.",
+    "Decrypt":
+        "Decrypt",
+    "Enter password":
+        "Inserisci la password",
+    "Loading…": "Loading…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English)."
+}

+ 151 - 0
i18n/no.json

@@ -0,0 +1,151 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s er en minimalistisk, åpen kildekode, elektronisk tilgjengelig pastebin hvor serveren ikke har kunnskap om dataene som limes inn. Dataene krypteres/dekrypteres <i>i nettleseren</i> ved hjelp av 256 bits AES. Mer informasjon om prosjektet på  <a href=\"https://privatebin.info/\">prosjektsiden</a>.",
+    "Because ignorance is bliss":
+        "Fordi uvitenhet er lykke",
+    "en": "no",
+    "Paste does not exist, has expired or has been deleted.":
+        "Innlegget eksisterer ikke, er utløpt eller har blitt slettet.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "Beklager, %s krever php 5.3.0 eller nyere for å kjøre.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s krever konfigurasjonsdel [%s] å være til stede i konfigurasjonsfilen .",
+    "Please wait %d seconds between each post.":
+        "Vennligst vent %d sekunder mellom hvert innlegg.",
+    "Paste is limited to %s of encrypted data.":
+        "Innlegg er begrenset til %s av kryptert data.",
+    "Invalid data.":
+        "Ugyldige data.",
+    "You are unlucky. Try again.":
+        "Du er uheldig. Prøv igjen.",
+    "Error saving comment. Sorry.":
+        "Beklager, det oppstod en feil ved lagring kommentar.",
+    "Error saving paste. Sorry.":
+        "Beklager, det oppstod en feil ved lagring innlegg.",
+    "Invalid paste ID.":
+        "Feil innlegg ID.",
+    "Paste is not of burn-after-reading type.":
+        "Innlegg er ikke av type slett-etter-lesing.",
+    "Wrong deletion token. Paste was not deleted.":
+        "Feil slettingsnøkkel. Innlegg ble ikke fjernet.",
+    "Paste was properly deleted.":
+        "Innlegget er slettet.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "Javascript kreves for at %s skal fungere<br />Beklager ulempene.",
+    "%s requires a modern browser to work.":
+        "%s krever en moderne nettleser for å fungere.",
+    "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
+        "Fortsatt bruker av Internet Explorer? Gjør deg selv en tjeneste og bytt til en moderne nettleser:",
+    "New":
+        "Ny",
+    "Send":
+        "Send",
+    "Clone":
+        "Kopier",
+    "Raw text":
+        "Ren tekst",
+    "Expires":
+        "Utgår",
+    "Burn after reading":
+        "Slett etter lesing",
+    "Open discussion":
+        "Åpen diskusjon",
+    "Password (recommended)":
+        "Passord (anbefalt)",
+    "Discussion":
+        "Diskusjon",
+    "Toggle navigation":
+        "Veksle navigasjon",
+    "%d seconds": ["%d sekund", "%d sekunder"],
+    "%d minutes": ["%d minutt", "%d minutter"],
+    "%d hours": ["%d time", "%d timer"],
+    "%d days": ["%d dag", "%d dager"],
+    "%d weeks": ["%d uke", "%d uker"],
+    "%d months": ["%d måned", "%d måneder"],
+    "%d years": ["%d år", "%d år"],
+    "Never":
+        "Aldri",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
+        "Merk: Dette er en test tjeneste: Data kan slettes når som helst. Kattunger vil dø hvis du misbruker denne tjenesten.",
+    "This document will expire in %d seconds.":
+        ["Dette dokumentet vil utløpe om %d sekund.", "Dette dokumentet vil utløpe om %d sekunder."],
+    "This document will expire in %d minutes.":
+        ["Dette dokumentet vil utløpe om %d minutt.", "Dette dokumentet vil utløpe om %d minutter."],
+    "This document will expire in %d hours.":
+        ["Dette dokumentet vil utløpe om %d time.", "Dette dokumentet vil utløpe om %d timer."],
+    "This document will expire in %d days.":
+        ["Dette dokumentet vil utløpe om %d dag.", "Dette dokumentet vil utløpe om %d dager."],
+    "This document will expire in %d months.":
+        ["Dette dokumentet vil utløpe om %d måned.", "Dette dokumentet vil utløpe om %d måneder."],
+    "Please enter the password for this paste:":
+        "Vennligst skriv inn passordet for dette innlegget:",
+    "Could not decrypt data (Wrong key?)":
+        "Kunne ikke dekryptere data (Feil nøkkel?)",
+    "Could not delete the paste, it was not stored in burn after reading mode.":
+        "Kan ikke slette innlegget, det ble ikke lagret i slett-etter-les modus.",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
+        "KUN FOR DINE ØYNE. Ikke lukk dette vinduet, denne meldingen kan ikke bli vist igjen.",
+    "Could not decrypt comment; Wrong key?":
+        "Kan ikke dekryptere kommentar; Feil nøkkel?",
+    "Reply":
+        "Svar",
+    "Anonymous":
+        "Anonym",
+    "Anonymous avatar (Vizhash of the IP address)":
+        "Anonym avatar (Vizhash av IP adressen)",
+    "Add comment":
+        "Legg til kommentar",
+    "Optional nickname…":
+        "Valgfritt kallenavn…",
+    "Post comment":
+        "Send kommentar",
+    "Sending comment…":
+        "Sender Kommentar…",
+    "Comment posted.":
+        "Kommentar sendt.",
+    "Could not refresh display: %s":
+        "Kunne ikke oppdatere skjermen: %s",
+    "unknown status":
+        "ukjent status",
+    "server error or not responding":
+        "server feilet eller svarer ikke",
+    "Could not post comment: %s":
+        "Kunne ikke sende kommentar: %s",
+    "Sending paste (Please move your mouse for more entropy)…":
+        "Sender innlegg (Flytt musen for mere entropi)…",
+    "Sending paste…":
+        "Sender innlegg…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Ditt innlegg er <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Trykk [Ctrl]+[c] for å kopiere)</span>",
+    "Delete data":
+        "Slett data",
+    "Could not create paste: %s":
+        "Kunne ikke opprette innlegg: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
+        "Kan ikke dekryptere innlegg: Dekrypteringsnøkkelen mangler i adressen (Har du bruket en redirector eller en URL forkorter som fjerner en del av addressen?)",
+    "Format": "Format",
+    "Plain Text": "Ren Tekst",
+    "Source Code": "Kildekode",
+    "Markdown": "Oppmerket",
+    "Download attachment": "Last ned vedlegg",
+    "Cloned file attached.": "Kopier vedlegg.",
+    "Attach a file": "Legg til fil",
+    "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.",
+    "Invalid attachment.": "Ugyldig vedlegg.",
+    "Options": "Alternativer",
+    "Shorten URL": "Adresse-forkorter",
+    "Editor": "Rediger",
+    "Preview": "Forhåndsvis",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s krever at PATH ender på \"%s\". Vennligst oppdater PATH i index.php.",
+    "Decrypt":
+        "Dekrypter",
+    "Enter password":
+        "Skriv inn passord",
+    "Loading…": "Laster…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">ofte stilte spørsmål</a> for informasjon om feilsøking."
+}

+ 160 - 0
i18n/oc.json

@@ -0,0 +1,160 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%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 <i>dins lo navigator</i> per un chiframent AES 256 bits. Mai informacions sus <a href=\"https://privatebin.info/\">la pagina del projècte</a>.",
+    "Because ignorance is bliss":
+        "Perque l'ignorància es bonaür",
+    "en": "oc",
+    "Paste does not exist, has expired or has been deleted.":
+        "Lo tèxte existís pas, a expirat, o es estat suprimit.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "O planhèm, %s necessita php 5.3.0 o superior per foncionar.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s fa besonh de la seccion de configuracion [%s] dins lo fichièr de configuracion per foncionar.",
+    "Please wait %d seconds between each post.":
+        "Mercés d'esperar %d segondas entre cada publicacion.",
+    "Paste is limited to %s of encrypted data.":
+        "Lo tèxte es limitat a %s de donadas chifradas.",
+    "Invalid data.":
+        "Donadas invalidas.",
+    "You are unlucky. Try again.":
+        "Pas cap de fortuna. Tornatz ensajar.",
+    "Error saving comment. Sorry.":
+        "Error al moment de salvagardar lo comentari. O planhèm.",
+    "Error saving paste. Sorry.":
+        "Error al moment de salvagardar lo tèxte. O planhèm.",
+    "Invalid paste ID.":
+        "ID del tèxte invalid.",
+    "Paste is not of burn-after-reading type.":
+        "Lo tèxte es pas del tip \"Escafar aprèp lectura\".",
+    "Wrong deletion token. Paste was not deleted.":
+        "Geton de supression incorrècte. Lo tèxte es pas estat suprimit.",
+    "Paste was properly deleted.":
+        "Lo tèxte es estat correctament suprimit.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "JavaScript es requesit per far foncionar %s. <br />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 :",
+    "New":
+        "Nòu",
+    "Send":
+        "Mandar",
+    "Clone":
+        "Clonar",
+    "Raw text":
+        "Tèxte brut",
+    "Expires":
+        "Expira",
+    "Burn after reading":
+        "Escafar aprèp lectura",
+    "Open discussion":
+        "Autorizar la discussion",
+    "Password (recommended)":
+        "Senhal (recomandat)",
+    "Discussion":
+        "Discussion",
+    "Toggle navigation":
+        "Virar la navigacion",
+    "%d seconds": ["%d segonda", "%d segondas"],
+    "%d minutes": ["%d minuta", "%d minutas"],
+    "%d hours": ["%d ora", "%d oras"],
+    "%d days": ["%d jorn", "%d jorns"],
+    "%d weeks": ["%d setmana", "%d setmanas"],
+    "%d months": ["%d mes", "%d meses"],
+    "%d years": ["%d an", "%d ans"],
+    "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.",
+    "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.":
+        ["Ce document expirera dans %d minute.", "Aqueste document expirarà dins %d minutas."],
+    "This document will expire in %d hours.":
+        ["Ce document expirera dans %d heure.", "Aqueste document expirarà dins %d oras."],
+    "This document will expire in %d days.":
+        ["Ce document expirera dans %d jour.", "Aqueste document expirarà dins %d jorns."],
+    "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 :",
+    "Could not decrypt data (Wrong key?)":
+        "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 ?",
+    "Reply":
+        "Respondre",
+    "Anonymous":
+        "Anonime",
+    "Anonymous avatar (Vizhash of the IP address)":
+        "Avatar anonime (Vizhash de l'adreça IP)",
+    "Add comment":
+        "Apondre un comentari",
+    "Optional nickname…":
+        "Escais opcional…",
+    "Post comment":
+        "Mandar lo comentari",
+    "Sending comment…":
+        "Mandadís del comentari…",
+    "Comment posted.":
+        "Comentari mandat.",
+    "Could not refresh display: %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",
+    "Sending paste (Please move your mouse for more entropy)…":
+        "Mandadís del tèxte (Mercés de bolegar vòstra mirga per mai entropia)…",
+    "Sending paste…":
+        "Mandadís del tèxte…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Vòstre tèxte es disponible a l'adreça <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Picatz sus [Ctrl]+[c] per copiar)</span>",
+    "Delete data":
+        "Supprimir las donadas del tèxte",
+    "Could not create paste: %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 ?)",
+    "B": "o",
+    "KiB": "Kio",
+    "MiB": "Mio",
+    "GiB": "Gio",
+    "TiB": "Tio",
+    "PiB": "Pio",
+    "EiB": "Eio",
+    "ZiB": "Zio",
+    "YiB": "Yio",
+    "Format": "Format",
+    "Plain Text": "Tèxte brut",
+    "Source Code": "Còdi font",
+    "Markdown": "Markdown",
+    "Download attachment": "Telecargar la pèça junta",
+    "Cloned file attached.": "Clonar lo fichièr junt.",
+    "Attach a file": "Juntar un fichièr ",
+    "Remove attachment": "Levar la pèca junta",
+    "Your browser does not support uploading encrypted files. Please use a newer browser.":
+        "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",
+    "Editor": "Editar",
+    "Preview": "Previsualizar",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s demanda que lo PATH termine en \"%s\". Mercés de metre a jorn lo PATH dins vòstre index.php.",
+    "Decrypt":
+        "Deschifrar",
+    "Enter password":
+        "Picatz lo senhal",
+    "Loading…": "Cargament…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">aquesta FAQ per las solucions</a> (en Anglés)."
+}

+ 34 - 27
i18n/pl.json

@@ -1,11 +1,16 @@
 {
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s jest minimalistycznym, otwartoźródłowym serwisem typu pastebin, w którym serwer nie ma jakichkolwiek informacji o tym, co jest wklejane. Dane są szyfrowane i deszyfrowane <i>w przeglądarce</i> z użyciem 256-bitowego klucza AES. Więcej informacji na <a href=\"https://privatebin.info/\">stronie projektu</a>.",
+    "Because ignorance is bliss":
+        "Ponieważ ignorancja jest cnotą",
     "en": "pl",
     "Paste does not exist, has expired or has been deleted.":
         "Wklejka nie istnieje, wygasła albo została usunięta.",
-    "PrivateBin requires php 5.3.0 or above to work. Sorry.":
-        "PrivateBin wymaga PHP w wersji 5.3.0 lub nowszej, sorry.",
-    "PrivateBin requires configuration section [%s] to be present in configuration file.":
-        "PrivateBin wymaga obecności sekcji [%s] w pliku konfiguracyjnym.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s wymaga PHP w wersji 5.3.0 lub nowszej, sorry.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s wymaga obecności sekcji [%s] w pliku konfiguracyjnym.",
     "Please wait %d seconds between each post.":
         "Poczekaj %d sekund pomiędzy każdą wklejką.",
     "Paste is limited to %s of encrypted data.":
@@ -26,15 +31,10 @@
         "Nieprawidłowy token usuwania. Wklejka nie została usunięta.",
     "Paste was properly deleted.":
         "Wklejka usunięta poprawnie.",
-    "PrivateBin": "PrivateBin",
-    "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
-        "PrivateBin jest minimalistycznym, otwartoźródłowym serwisem typu pastebin, w którym serwer nie ma jakichkolwiek informacji o tym, co jest wklejane. Dane są szyfrowane i deszyfrowane <i>w przeglądarce</i> z użyciem 256-bitowego klucza AES. Więcej informacji na <a href=\"https://privatebin.info/\">stronie projektu</a>.",
-    "Because ignorance is bliss":
-        "Ponieważ ignorancja jest cnotą",
-    "Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.":
-        "Do działania PrivateBina jest wymagany JavaScript. Przepraszamy za tę niedogodność.",
-    "PrivateBin requires a modern browser to work.":
-        "PrivateBin wymaga do działania nowoczesnej przeglądarki.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "Do działania %sa jest wymagany JavaScript. Przepraszamy za tę niedogodność.",
+    "%s requires a modern browser to work.":
+        "%s wymaga do działania nowoczesnej przeglądarki.",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
         "Cały czas używasz Internet Explorera? Zrób sobie przysługę, przesiądź się na nowoczesną przeglądarkę:",
     "New":
@@ -59,11 +59,11 @@
         "Przełącz nawigację",
     "%d seconds": ["%d second", "%d second", "%d second"],
     "%d minutes": ["%d minut", "%d minut", "%d minut"],
-    "%d hours": ["%d godzina", "%d godzina", "godzinę"],
+    "%d hours": ["%d godzina", "%d godzina", "%d godzinę"],
     "%d days": ["%d dzień", "%d dzień", "%d dzień"],
-    "%d weeks": ["%d tydzień", "%d tydzień", "tydzień"],
-    "%d months": ["%d miesiąc", "%d miesiąc", "miesiąc"],
-    "%d years": ["%d rok", "%d rok", "rok"],
+    "%d weeks": ["%d tydzień", "%d tydzień", "%d tydzień"],
+    "%d months": ["%d miesiąc", "%d miesiąc", "%d miesiąc"],
+    "%d years": ["%d rok", "%d rok", "%d rok"],
     "Never":
         "nigdy",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
@@ -96,12 +96,12 @@
         "Anonimowy avatar (Vizhash z adresu IP)",
     "Add comment":
         "Dodaj komentarz",
-    "Optional nickname...":
-        "Opcjonalny nick...",
+    "Optional nickname":
+        "Opcjonalny nick",
     "Post comment":
         "Wyślij komentarz",
-    "Sending comment...":
-        "Wysyłanie komentarza...",
+    "Sending comment":
+        "Wysyłanie komentarza",
     "Comment posted.":
         "Wysłano komentarz.",
     "Could not refresh display: %s":
@@ -112,10 +112,10 @@
         "bląd serwera lub brak odpowiedzi",
     "Could not post comment: %s":
         "Nie udało się wysłać komentarza: %s",
-    "Sending paste (Please move your mouse for more entropy)...":
-        "Wysyłanie wklejki (proszę poruszać myszą aby uzyskać większą entropię)...",
-    "Sending paste...":
-        "Wysyłanie wklejki...",
+    "Sending paste (Please move your mouse for more entropy)":
+        "Wysyłanie wklejki (proszę poruszać myszą aby uzyskać większą entropię)",
+    "Sending paste":
+        "Wysyłanie wklejki",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
         "Twoja wklejka to <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(wciśnij [Ctrl]+[c] aby skopiować)</span>",
     "Delete data":
@@ -139,6 +139,13 @@
     "Shorten URL": "Skróć adres URL",
     "Editor": "Edytować",
     "Preview": "Zapowiedź",
-    "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
-        "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php."
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.",
+    "Decrypt":
+        "Decrypt",
+    "Enter password":
+        "Wpisz hasło",
+    "Loading…": "Loading…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English)."
 }

+ 151 - 0
i18n/pt.json

@@ -0,0 +1,151 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s é um serviço minimalista e de código aberto do tipo \"pastebin\", em que o servidor tem zero conhecimento dos dados copiados. Os dados são cifrados e decifrados <i>no navegador</i> usando 256 bits AES. Mais informações na <a href=\"https://privatebin.info/\">página do projeto</a>.",
+    "Because ignorance is bliss":
+        "Porque a ignorância é uma benção",
+    "en": "pt",
+    "Paste does not exist, has expired or has been deleted.":
+        "A cópia não existe, expirou ou já foi excluída.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s requer php 5.3.0 ou superior para funcionar. Desculpa.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s requer que a seção de configuração [% s] esteja no arquivo de configuração.",
+    "Please wait %d seconds between each post.":
+        "Por favor espere %d segundos entre cada publicação.",
+    "Paste is limited to %s of encrypted data.":
+        "A cópia está limitada a %s de dados cifrados.",
+    "Invalid data.":
+        "Dados inválidos.",
+    "You are unlucky. Try again.":
+        "Você é azarado. Tente novamente",
+    "Error saving comment. Sorry.":
+        "Erro ao salvar comentário. Desculpa.",
+    "Error saving paste. Sorry.":
+        "Erro ao salvar cópia. Desculpa.",
+    "Invalid paste ID.":
+        "ID de cópia inválido.",
+    "Paste is not of burn-after-reading type.":
+        "Cópia não é do tipo \"queime após ler\".",
+    "Wrong deletion token. Paste was not deleted.":
+        "Token de remoção inválido. A cópia não foi excluída.",
+    "Paste was properly deleted.":
+        "A cópia foi devidamente excluída.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "JavaScript é necessário para que %s funcione.<br />Pedimos desculpas pela inconveniência.",
+    "%s requires a modern browser to work.":
+        "%s requer um navegador moderno para funcionar.",
+    "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
+        "Ainda usando Internet Explorer? Faça-se um favor, mude para um navegador moderno:",
+    "New":
+        "Novo",
+    "Send":
+        "Enviar",
+    "Clone":
+        "Clonar",
+    "Raw text":
+        "Texto sem formato",
+    "Expires":
+        "Expirar em",
+    "Burn after reading":
+        "Queime após ler",
+    "Open discussion":
+        "Discussão aberta",
+    "Password (recommended)":
+        "Senha (recomendada)",
+    "Discussion":
+        "Discussão",
+    "Toggle navigation":
+        "Mudar navegação",
+    "%d seconds": ["%d segundo", "%d segundos"],
+    "%d minutes": ["%d minuto", "%d minutos"],
+    "%d hours": ["%d hora", "%d horas"],
+    "%d days": ["%d dia", "%d dias"],
+    "%d weeks": ["%d semana", "%d semanas"],
+    "%d months": ["%d mês", "%d meses"],
+    "%d years": ["%d ano", "%d anos"],
+    "Never":
+        "Nunca",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
+        "Nota: Este é um serviço de teste. Dados podem ser perdidos a qualquer momento. Gatinhos morrerão se você abusar desse serviço.",
+    "This document will expire in %d seconds.":
+        ["Este documento irá expirar em um segundo.", "Este documento irá expirar em %d segundos."],
+    "This document will expire in %d minutes.":
+        ["Este documento irá expirar em um minuto.", "Este documento irá expirar em %d minutos."],
+    "This document will expire in %d hours.":
+        ["Este documento irá expirar em uma hora.", "Este documento irá expirar em %d horas."],
+    "This document will expire in %d days.":
+        ["Este documento irá expirar em um dia.", "Este documento irá expirar em %d dias."],
+    "This document will expire in %d months.":
+        ["Este documento irá expirar em um mês.", "Este documento irá expirar em %d meses."],
+    "Please enter the password for this paste:":
+        "Por favor, digite a senha para essa cópia:",
+    "Could not decrypt data (Wrong key?)":
+        "Não foi possível decifrar os dados (Chave errada?)",
+    "Could not delete the paste, it was not stored in burn after reading mode.":
+        "Não foi possível excluir a cópia, ela não foi salva no modo de \"queime após ler\".",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
+        "APENAS PARA SEUS OLHOS. Não feche essa janela, essa mensagem não pode ser exibida novamente.",
+    "Could not decrypt comment; Wrong key?":
+        "Não foi possível decifrar o comentário; Chave errada?",
+    "Reply":
+        "Responder",
+    "Anonymous":
+        "Anônimo",
+    "Avatar generated from IP address":
+        "Avatar (do endereço IP)",
+    "Add comment":
+        "Adicionar comentário",
+    "Optional nickname…":
+        "Apelido opcional…",
+    "Post comment":
+        "Publicar comentário",
+    "Sending comment…":
+        "Enviando comentário…",
+    "Comment posted.":
+        "Comentário publicado.",
+    "Could not refresh display: %s":
+        "Não foi possível atualizar a tela: %s",
+    "unknown status":
+        "Estado desconhecido",
+    "server error or not responding":
+        "Servidor em erro ou não responsivo",
+    "Could not post comment: %s":
+        "Não foi possível publicar o comentário: %s",
+    "Please move your mouse for more entropy…":
+        "Por favor, mova o mouse para maior entropia…",
+    "Sending paste…":
+        "Enviando cópia…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Sua cópia é <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Pressione [Ctrl]+[c] para copiar)</span>",
+    "Delete data":
+        "Excluir dados",
+    "Could not create paste: %s":
+        "Não foi possível criar cópia: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
+        "Não foi possível decifrar a cópia: chave de decriptografia ausente na URL (Você utilizou um redirecionador ou encurtador de URL que removeu parte dela?)",
+    "Format": "Formato",
+    "Plain Text": "Texto sem formato",
+    "Source Code": "Código fonte",
+    "Markdown": "Markdown",
+    "Download attachment": "Baixar anexo",
+    "Cloned: '%s'": "Clonado: '%s'",
+    "Attach a file": "Anexar um arquivo",
+    "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.",
+    "Invalid attachment.": "Anexo inválido.",
+    "Options": "Opções",
+    "Shorten URL": "Encurtar URL",
+    "Editor": "Editor",
+    "Preview": "Visualizar",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s requer que o PATH termine em \"%s\". Por favor, atualize o PATH em seu index.php.",
+    "Decrypt":
+        "Decifrar",
+    "Enter password":
+        "Digite a senha",
+    "Loading…": "Carregando…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "Caso essa mensagem nunca desapareça, por favor veja <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">este FAQ para saber como resolver os problemas</a>."
+}

+ 159 - 0
i18n/ru.json

@@ -0,0 +1,159 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s это минималистичный Open Source проект для создания заметок, где сервер не знает ничего о сохраняемых данных. Данные шифруются/расшифровываются <i>в браузере</i> с использованием 256 битного шифрования AES. Подробнее можно узнать на <a href=\"https://privatebin.info/\">сайте проекта</a>.",
+    "Because ignorance is bliss":
+        "Потому что неведение - благо",
+    "en": "ru",
+    "Paste does not exist, has expired or has been deleted.":
+        "Запись не существует, просрочена или была удалена.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "Для работы %s требуется PHP 5.3.0 или выше. Извините.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s необходимо наличие секции [%s] в конфигурационном файле.",
+    "Please wait %d seconds between each post.":
+        ["Пожалуйста ожидайте %d секунду между каждыми записями.", "Пожалуйста ожидайте %d секунды между каждыми записями.", "Пожалуйста ожидайте %d секунд между каждыми записями."],
+    "Paste is limited to %s of encrypted data.":
+        "Размер записи ограничен %s зашифрованных данных.",
+    "Invalid data.":
+        "Неверные данные.",
+    "You are unlucky. Try again.":
+        "Вам не повезло. Попробуйте еще раз.",
+    "Error saving comment. Sorry.":
+        "Ошибка при сохранении комментария. Извините.",
+    "Error saving paste. Sorry.":
+        "Ошибка при сохранении записи. Извините.",
+    "Invalid paste ID.":
+        "Неверный ID записи.",
+    "Paste is not of burn-after-reading type.":
+        "Тип записи не \"Удалить после прочтения\".",
+    "Wrong deletion token. Paste was not deleted.":
+        "Неверный ключ удаления записи. Запись не удалена",
+    "Paste was properly deleted.":
+        "Запись была успешно удалена.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "Для работы %s требуется включенный JavaScript.<br />Приносим извинения за неудобства..",
+    "%s requires a modern browser to work.":
+        "Для работы %s требуется более современный браузер.",
+    "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
+        "До сих пор используете Internet Explorer? Пожалейте себя, перейдите на более современный браузер:",
+    "New":
+        "Новая запись",
+    "Send":
+        "Отправить",
+    "Clone":
+        "Дублировать",
+    "Raw text":
+        "Исходный текст",
+    "Expires":
+        "Удалить через",
+    "Burn after reading":
+        "Удалить после прочтения",
+    "Open discussion":
+        "Открыть обсуждение",
+    "Password (recommended)":
+        "Пароль (рекомендуется)",
+    "Discussion":
+        "Обсуждение",
+    "Toggle navigation":
+        "Переключить навигацию",
+    "%d seconds": ["%d секунду", "%d секунды", "%d секунд"],
+    "%d minutes": ["%d минуту", "%d минуты", "%d минут"],
+    "%d hours": ["%d час", "%d часа", "%d часов"],
+    "%d days": ["%d день", "%d дня", "%d дней"],
+    "%d weeks": ["%d неделю", "%d недели", "%d недель"],
+    "%d months": ["%d месяц", "%d месяца", "%d месяцев"],
+    "%d years": ["%d год", "%d года", "%d лет"],
+    "Never":
+        "Никогда",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.":
+        "Примечание: Этот сервис тестовый: Данные могут быть удалены в любое время. Котята умрут, если вы будете злоупотреблять серсисом.",
+    "This document will expire in %d seconds.":
+        ["Документ будет удален через %d секунду.", "Документ будет удален через %d секунды.", "Документ будет удален через %d секунд."],
+    "This document will expire in %d minutes.":
+        ["Документ будет удален через %d минуту.", "Документ будет удален через %d минуты.", "Документ будет удален через %d минут."],
+    "This document will expire in %d hours.":
+        ["Документ будет удален через %d час.", "Документ будет удален через %d часа.", "Документ будет удален через %d часов."],
+    "This document will expire in %d days.":
+        ["Документ будет удален через %d день.", "Документ будет удален через %d дня.", "Документ будет удален через %d дней."],
+    "This document will expire in %d months.":
+        ["Документ будет удален через %d месяц.", "Документ будет удален через %d месяца.", "Документ будет удален через %d месяцев."],
+    "Please enter the password for this paste:":
+        "Пожалуйста введите пароль от записи:",
+    "Could not decrypt data (Wrong key?)":
+        "Невозможно расшифровать данные (Неверный ключ?)",
+    "Could not delete the paste, it was not stored in burn after reading mode.":
+        "Невозможно удалить запись, она не была сохранена в режиме удаления после прочтения",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
+        "ТОЛЬКО ДЛЯ ВАШИХ ГЛАЗ. Не закрывайте это окно, это сообщение не может быть показано снова.",
+    "Could not decrypt comment; Wrong key?":
+        "Невозможно расшифровать комментарий; Неверный ключ?",
+    "Reply":
+        "Ответить",
+    "Anonymous":
+        "Аноним",
+    "Anonymous avatar (Vizhash of the IP address)":
+        "Анонимный аватар (Vizhash IP адреса)",
+    "Add comment":
+        "Добавить комментарий",
+    "Optional nickname…":
+        "Опциональный никнейм…",
+    "Post comment":
+        "Отправить комментарий",
+    "Sending comment…":
+        "Отправка комментария…",
+    "Comment posted.":
+        "Комментарий опубликован.",
+    "Could not refresh display: %s":
+        "Невозможно обновить данные: %s",
+    "unknown status":
+        "неизвестная причина",
+    "server error or not responding":
+        "ошибка сервера или нет ответа",
+    "Could not post comment: %s":
+        "Не удалось опубликовать комментарий: %s",
+    "Sending paste (Please move your mouse for more entropy)…":
+        "Отправка записи (Пожалуйста двигайте мышкой для большей энтропии)…",
+    "Sending paste…":
+        "Отправка записи…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
+        "Ссылка на запись <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Нажмите [Ctrl]+[c] чтобы скопировать ссылку)</span>",
+    "Delete data":
+        "Удалить запись",
+    "Could not create paste: %s":
+        "Не удалось опубликовать запись: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
+        "Невозможно расшифровать запись: Ключ расшифровки отсутствует в ссылке (Может быть вы используете сокращатель ссылок, который удаляет часть ссылки?)",
+    "B": "байт",
+    "KiB": "Кбайт",
+    "MiB": "Мбайт",
+    "GiB": "Гбайт",
+    "TiB": "Тбайт",
+    "PiB": "Пбайт",
+    "EiB": "Эбайт",
+    "ZiB": "Збайт",
+    "YiB": "Йбайт",
+    "Format": "Формат",
+    "Plain Text": "Обычный текст",
+    "Source Code": "Исходный код",
+    "Markdown": "Язык разметки",
+    "Download attachment": "Скачать прикрепленный файл",
+    "Cloned file attached.": "Дубль файла прикреплен.",
+    "Attach a file": "Прикрепить файл",
+    "Remove attachment": "Удалить вложение",
+    "Your browser does not support uploading encrypted files. Please use a newer browser.":
+        "Ваш браузер не поддерживает отправку зашифрованных файлов. Используйте более новый браузер.",
+    "Invalid attachment.": "Неизвестное вложение.",
+    "Options": "Опции",
+    "Shorten URL": "Короткая ссылка",
+    "Editor": "Редактор",
+    "Preview": "Предпросмотр",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "Переменная PATH необходима %s в конце \"%s\". Пожалуйста обновите переменную PATH в вашем index.php.",
+    "Decrypt":
+        "Расшифровать",
+    "Enter password":
+        "Введите пароль",
+    "Uploading paste… Please wait.":
+        "Отправка записи… Пожалуйста подождите."
+}

+ 29 - 22
i18n/sl.json

@@ -1,11 +1,16 @@
 {
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s je minimalističen, odprtokodni spletni 'pastebin', kjer server ne ve ničesar o prilepljenih podatkih. Podatki so zakodirani/odkodirani <i>v brskalniku</i> z uporabo 256 bitnega AES. Več informacij na < href=\"https://privatebin.info/\">spletni strani projekta.</a>.",
+    "Because ignorance is bliss":
+        "Ker kar ne veš ne boli.",
     "en": "sl",
     "Paste does not exist, has expired or has been deleted.":
         "Prilepek ne obstaja, mu je potekla življenjska doba, ali pa je izbrisan.",
-    "PrivateBin requires php 5.3.0 or above to work. Sorry.":
-        "Oprosti, PrivateBin za delovanje potrebuje vsaj php 5.3.0.",
-    "PrivateBin requires configuration section [%s] to be present in configuration file.":
-        "PrivateBin potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "Oprosti, %s za delovanje potrebuje vsaj php 5.3.0.",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "Please wait %d seconds between each post.":
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
     "Paste is limited to %s of encrypted data.":
@@ -26,15 +31,10 @@
         "Napačen token za izbris. Prilepek ni bil izbrisan..",
     "Paste was properly deleted.":
         "Prilepek je uspešno izbrisan.",
-    "PrivateBin": "PrivateBin",
-    "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
-        "PrivateBin je minimalističen, odprtokodni spletni 'pastebin', kjer server ne ve ničesar o prilepljenih podatkih. Podatki so zakodirani/odkodirani <i>v brskalniku</i> z uporabo 256 bitnega AES. Več informacij na < href=\"https://privatebin.info/\">spletni strani projekta.</a>.",
-    "Because ignorance is bliss":
-        "Ker kar ne veš ne boli.",
-    "Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.":
-        "Da PrivateBin deluje, moraš vklopiti Javascript.<br />Oprosti za povročene nevšečnosti.",
-    "PrivateBin requires a modern browser to work.":
-        "PrivateBin za svoje delovanje potrebuje moderen brskalnik.",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "Da %s deluje, moraš vklopiti JavaScript.<br />Oprosti za povročene nevšečnosti.",
+    "%s requires a modern browser to work.":
+        "%s za svoje delovanje potrebuje moderen brskalnik.",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
         "Še vedno uporabljaš Internet Explorer? Naredi si uslugo, preklopi na moderen brskalnik:",
     "New":
@@ -96,12 +96,12 @@
         "Anonimen avatar (Vizhash IP naslova)",
     "Add comment":
         "Dodaj komentar",
-    "Optional nickname...":
+    "Optional nickname":
         "Uporabniško ime (lahko izpustiš)",
     "Post comment":
         "Objavi komentar",
-    "Sending comment...":
-        "Pošiljam komentar ...",
+    "Sending comment":
+        "Pošiljam komentar ",
     "Comment posted.":
         "Komentar poslan.",
     "Could not refresh display: %s":
@@ -112,10 +112,10 @@
         "napaka na strežniku, ali pa se strežnik ne odziva",
     "Could not post comment: %s":
         "Komentarja ni bilo mogoče objaviti : %s",
-    "Sending paste (Please move your mouse for more entropy)...":
-        "Pošiljam prilepek (prosim premakni svojo miško za več entropije) ...",
-    "Sending paste...":
-        "Pošiljam prilepek...",
+    "Sending paste (Please move your mouse for more entropy)":
+        "Pošiljam prilepek (prosim premakni svojo miško za več entropije) ",
+    "Sending paste":
+        "Pošiljam prilepek",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
         "Tvoj prilepek je dostopen na naslovu: <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Pritisni [Ctrl]+[c] ali [Cmd] + [c] in skopiraj)</span>",
     "Delete data":
@@ -148,6 +148,13 @@
     "Shorten URL": "Skrajšajte URL",
     "Editor": "Uredi",
     "Preview": "Predogled",
-    "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
-        "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php."
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.",
+    "Decrypt":
+        "Decrypt",
+    "Enter password":
+        "Prosim vnesi geslo",
+    "Loading…": "Loading…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a> (in English)."
 }

+ 32 - 25
i18n/zh.json

@@ -1,11 +1,16 @@
 {
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
+        "%s是一个极简,开源,对粘贴内容毫不知情的在线粘贴板,数据<i>在浏览器内</i>进行AES-256加密。更多信息请查看<a href=\"https://privatebin.info/\">项目主页</a>。",
+    "Because ignorance is bliss":
+        "因为无知是福",
     "en": "zh",
     "Paste does not exist, has expired or has been deleted.":
         "粘贴不存在,已过期或者已被删除。",
-    "PrivateBin requires php 5.3.0 or above to work. Sorry.":
-        "PrivateBin需要工作于PHP 5.3.0及以上版本,抱歉。",
-    "PrivateBin requires configuration section [%s] to be present in configuration file.":
-        "PrivateBin需要设置配置文件中 [%s] 的部分。",
+    "%s requires php 5.3.0 or above to work. Sorry.":
+        "%s需要工作于PHP 5.3.0及以上版本,抱歉。",
+    "%s requires configuration section [%s] to be present in configuration file.":
+        "%s需要设置配置文件中 [%s] 的部分。",
     "Please wait %d seconds between each post.":
         "每 %d 秒只能粘贴一次。",
     "Paste is limited to %s of encrypted data.":
@@ -26,15 +31,10 @@
         "错误的删除token,粘贴没有被删除。",
     "Paste was properly deleted.":
         "粘贴已被正确删除。",
-    "PrivateBin": "PrivateBin",
-    "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
-        "PrivateBin是一个极简,开源,对粘贴内容毫不知情的在线粘贴板,数据<i>在浏览器内</i>进行AES-256加密。更多信息请查看<a href=\"https://privatebin.info/\">项目主页</a>。",
-    "Because ignorance is bliss":
-        "因为无知是福",
-    "Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.":
-        "PrivateBin需要JavaScript来进行加解密。<br />带来的不便敬请谅解。",
-    "PrivateBin requires a modern browser to work.":
-        "PrivateBin需要工作于现代化的浏览器。",
+    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+        "%s需要JavaScript来进行加解密。<br />带来的不便敬请谅解。",
+    "%s requires a modern browser to work.":
+        "%s需要工作于现代化的浏览器。",
     "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:":
         "还在使用Internet Explorer?帮自己个忙,换上一个现代化的浏览器:",
     "New":
@@ -96,12 +96,12 @@
         "匿名头像 (由IP地址生成Vizhash)",
     "Add comment":
         "添加评论",
-    "Optional nickname...":
-        "可选昵称...",
+    "Optional nickname":
+        "可选昵称",
     "Post comment":
         "评论",
-    "Sending comment...":
-        "评论发送中...",
+    "Sending comment":
+        "评论发送中",
     "Comment posted.":
         "评论已发送。",
     "Could not refresh display: %s":
@@ -112,10 +112,10 @@
         "服务器错误或无回应",
     "Could not post comment: %s":
         "无法发送评论: %s",
-    "Sending paste (Please move your mouse for more entropy)...":
-        "粘贴提交中 (请移动鼠标以产生更多熵)...",
-    "Sending paste...":
-        "粘贴提交中...",
+    "Sending paste (Please move your mouse for more entropy)":
+        "粘贴提交中 (请移动鼠标以产生更多熵)",
+    "Sending paste":
+        "粘贴提交中",
     "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>":
         "您的粘贴的链接是<a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(按下 [Ctrl]+[c] 以复制)</span>",
     "Delete data":
@@ -137,8 +137,15 @@
     "Invalid attachment.": "无效的附件",
     "Options": "选项",
     "Shorten URL": "缩短链接",
-    "Editor": "編輯",
-    "Preview": "預習",
-    "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
-        "PrivateBin requires the PATH to end in a \"%s\". Please update the PATH in your index.php."
+    "Editor": "编辑",
+    "Preview": "预览",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.":
+        "%s 的 PATH 变量必须结束于 \"%s\"。 请修改你的 index.php 中的 PATH 变量。",
+    "Decrypt":
+        "解密",
+    "Enter password":
+        "输入密码",
+    "Loading…": "载入中…",
+    "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
+        "如果这个消息一直不消失,请参考 <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">这里的 FAQ 进行故障排除</a> (英文版)。"
 }

+ 0 - 0
android-chrome-192x192.png → img/android-chrome-192x192.png


+ 0 - 0
android-chrome-512x512.png → img/android-chrome-512x512.png


+ 0 - 0
apple-touch-icon.png → img/apple-touch-icon.png


+ 0 - 0
favicon-16x16.png → img/favicon-16x16.png


+ 0 - 0
favicon-32x32.png → img/favicon-32x32.png


+ 0 - 0
favicon.ico → img/favicon.ico


+ 0 - 0
mstile-144x144.png → img/mstile-144x144.png


+ 0 - 0
mstile-150x150.png → img/mstile-150x150.png


+ 0 - 0
mstile-310x150.png → img/mstile-310x150.png


+ 0 - 0
mstile-310x310.png → img/mstile-310x310.png


+ 0 - 0
mstile-70x70.png → img/mstile-70x70.png


+ 0 - 0
safari-pinned-tab.svg → img/safari-pinned-tab.svg


+ 1 - 1
index.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.0
+ * @version   1.1
  */
 
 // change this, if your php files and data is outside of your webservers document root

+ 7 - 0
js/.istanbul.yml

@@ -0,0 +1,7 @@
+---
+instrumentation:
+  excludes:
+    - jquery-3.1.1.js
+  baseline-file: ../tst/log/js-coverage-baseline.json
+reporting:
+  dir: ../tst/log/js-coverage-report

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 5
js/bootstrap-3.3.5.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 5 - 0
js/bootstrap-3.3.7.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 1
js/jquery-1.11.3.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
js/jquery-3.1.1.js


+ 3625 - 1176
js/privatebin.js

@@ -3,13 +3,14 @@
  *
  * a zero-knowledge paste bin
  *
- * @link      https://github.com/PrivateBin/PrivateBin
- * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
- * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
- * @version   1.0
+ * @see       {@link https://github.com/PrivateBin/PrivateBin}
+ * @copyright 2012 Sébastien SAUVAGE ({@link http://sebsauvage.net})
+ * @license   {@link https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
+ * @version   1.1
+ * @name      PrivateBin
+ * @namespace
  */
 
-'use strict';
 /** global: Base64 */
 /** global: FileReader */
 /** global: RawDeflate */
@@ -23,18 +24,62 @@
 // Immediately start random number generator collector.
 sjcl.random.startCollectors();
 
-$(function() {
+// main application start, called when DOM is fully loaded
+jQuery(document).ready(function() {
+    // run main controller
+    $.PrivateBin.Controller.init();
+});
+
+jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
+    'use strict';
+
     /**
-     * static helper methods
+     * static Helper methods
+     *
+     * @name Helper
+     * @class
      */
-    var helper = {
+    var Helper = (function () {
+        var me = {};
+
+        /**
+         * character to HTML entity lookup table
+         *
+         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
+         * @name Helper.entityMap
+         * @private
+         * @enum   {Object}
+         * @readonly
+         */
+        var entityMap = {
+            '&': '&amp;',
+            '<': '&lt;',
+            '>': '&gt;',
+            '"': '&quot;',
+            "'": '&#39;',
+            '/': '&#x2F;',
+            '`': '&#x60;',
+            '=': '&#x3D;'
+        };
+
+        /**
+         * cache for script location
+         *
+         * @name Helper.baseUri
+         * @private
+         * @enum   {string|null}
+         */
+        var baseUri = null;
+
         /**
-         * Converts a duration (in seconds) into human friendly approximation.
+         * converts a duration (in seconds) into human friendly approximation
          *
-         * @param int seconds
-         * @return array
+         * @name Helper.secondsToHuman
+         * @function
+         * @param  {number} seconds
+         * @return {Array}
          */
-        secondsToHuman: function(seconds)
+        me.secondsToHuman = function(seconds)
         {
             var v;
             if (seconds < 60)
@@ -60,193 +105,115 @@ $(function() {
             }
             v = Math.floor(seconds / (60 * 60 * 24 * 30));
             return [v, 'month'];
-        },
-
-        /**
-         * Converts an associative array to an encoded string
-         * for appending to the anchor.
-         *
-         * @param object associative_array Object to be serialized
-         * @return string
-         */
-        hashToParameterString: function(associativeArray)
-        {
-            var parameterString = '';
-            for (var key in associativeArray)
-            {
-                if(parameterString === '')
-                {
-                    parameterString = encodeURIComponent(key);
-                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
-                }
-                else
-                {
-                    parameterString += '&' + encodeURIComponent(key);
-                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
-                }
-            }
-            // padding for URL shorteners
-            parameterString += '&p=p';
-
-            return parameterString;
-        },
-
-        /**
-         * Converts a string to an associative array.
-         *
-         * @param string parameter_string String containing parameters
-         * @return object
-         */
-        parameterStringToHash: function(parameterString)
-        {
-            var parameterHash = {};
-            var parameterArray = parameterString.split('&');
-            for (var i = 0; i < parameterArray.length; i++)
-            {
-                var pair = parameterArray[i].split('=');
-                var key = decodeURIComponent(pair[0]);
-                var value = decodeURIComponent(pair[1]);
-                parameterHash[key] = value;
-            }
-
-            return parameterHash;
-        },
+        }
 
         /**
-         * Get an associative array of the parameters found in the anchor
+         * checks if a string is valid text (and not onyl whitespace)
          *
-         * @return object
+         * @name Helper.isValidText
+         * @function
+         * @param  {string} string
+         * @return {bool}
          */
-        getParameterHash: function()
+        me.isValidText = function(string)
         {
-            var hashIndex = window.location.href.indexOf('#');
-            if (hashIndex >= 0)
-            {
-                return this.parameterStringToHash(window.location.href.substring(hashIndex + 1));
-            }
-            else
-            {
-                return {};
-            }
-        },
+            return (string.length > 0 && $.trim(string) !== '')
+        }
 
         /**
-         * Text range selection.
-         * From: https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
+         * text range selection
          *
-         * @param string element : Indentifier of the element to select (id="").
+         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
+         * @name   Helper.selectText
+         * @function
+         * @param  {HTMLElement} element
          */
-        selectText: function(element)
+        me.selectText = function(element)
         {
-            var doc = document,
-                text = doc.getElementById(element),
-                range,
-                selection;
+            var range, selection;
 
             // MS
-            if (doc.body.createTextRange)
-            {
-                range = doc.body.createTextRange();
-                range.moveToElementText(text);
+            if (document.body.createTextRange) {
+                range = document.body.createTextRange();
+                range.moveToElementText(element);
                 range.select();
-            }
-            // all others
-            else if (window.getSelection)
-            {
+            } else if (window.getSelection){
                 selection = window.getSelection();
-                range = doc.createRange();
-                range.selectNodeContents(text);
+                range = document.createRange();
+                range.selectNodeContents(element);
                 selection.removeAllRanges();
                 selection.addRange(range);
             }
-        },
+        }
 
         /**
-         * Set text of a DOM element (required for IE)
-         * This is equivalent to element.text(text)
+         * set text of a jQuery element (required for IE),
          *
-         * @param object element : a DOM element.
-         * @param string text : the text to enter.
+         * @name   Helper.setElementText
+         * @function
+         * @param  {jQuery} $element - a jQuery element
+         * @param  {string} text - the text to enter
          */
-        setElementText: function(element, text)
+        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 = this.htmlEntities(text).replace(/\n/ig,'\r\n<br>');
-                element.html('<pre>'+html+'</pre>');
+                var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
+                $element.html('<pre>' + html + '</pre>');
             }
             // for other (sane) browsers:
             else
             {
-                element.text(text);
-            }
-        },
-
-        /**
-         * replace last child of element with message
-         *
-         * @param object element : a jQuery wrapped DOM element.
-         * @param string message : the message to append.
-         */
-        setMessage: function(element, message)
-        {
-            var content = element.contents();
-            if (content.length > 0)
-            {
-                content[content.length - 1].nodeValue = ' ' + message;
-            }
-            else
-            {
-                this.setElementText(element, message);
+                $element.text(text);
             }
-        },
+        }
 
         /**
-         * Convert URLs to clickable links.
+         * convert URLs to clickable links.
          * URLs to handle:
-         * <code>
+         * <pre>
          *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
-         *     http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-         *     http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-         * </code>
+         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         * </pre>
          *
-         * @param object element : a jQuery DOM element.
+         * @name   Helper.urls2links
+         * @function
+         * @param  {Object} element - a jQuery DOM element
          */
-        urls2links: function(element)
+        me.urls2links = function($element)
         {
             var markup = '<a href="$1" rel="nofollow">$1</a>';
-            element.html(
-                element.html().replace(
+            $element.html(
+                $element.html().replace(
                     /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
                     markup
                 )
             );
-            element.html(
-                element.html().replace(
+            $element.html(
+                $element.html().replace(
                     /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
                     markup
                 )
             );
-        },
+        }
 
         /**
          * minimal sprintf emulation for %s and %d formats
-         * From: https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914
          *
-         * @param string format
-         * @param mixed args one or multiple parameters injected into format string
-         * @return string
+         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
+         * @name   Helper.sprintf
+         * @function
+         * @param  {string} format
+         * @param  {...*} args - one or multiple parameters injected into format string
+         * @return {string}
          */
-        sprintf: function()
+        me.sprintf = function()
         {
-            var args = arguments;
-            if (typeof arguments[0] === 'object')
-            {
-                args = arguments[0];
-            }
-            var string = args[0],
+            var args = Array.prototype.slice.call(arguments);
+            var format = args[0],
                 i = 1;
-            return string.replace(/%((%)|s|d)/g, function (m) {
+            return format.replace(/%((%)|s|d)/g, function (m) {
                 // m is the matched format, e.g. %s, %d
                 var val;
                 if (m[2]) {
@@ -269,1334 +236,3816 @@ $(function() {
                 }
                 return val;
             });
-        },
+        }
 
         /**
          * get value of cookie, if it was set, empty string otherwise
-         * From: http://www.w3schools.com/js/js_cookies.asp
          *
-         * @param string cname
-         * @return string
+         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
+         * @name   Helper.getCookie
+         * @function
+         * @param  {string} cname
+         * @return {string}
          */
-        getCookie: function(cname) {
-            var name = cname + '=';
-            var ca = document.cookie.split(';');
+        me.getCookie = function(cname) {
+            var name = cname + '=',
+                ca = document.cookie.split(';');
             for (var i = 0; i < ca.length; ++i) {
                 var c = ca[i];
-                while (c.charAt(0) === ' ') c = c.substring(1);
+                while (c.charAt(0) === ' ')
+                {
+                    c = c.substring(1);
+                }
                 if (c.indexOf(name) === 0)
                 {
                     return c.substring(name.length, c.length);
                 }
             }
             return '';
-        },
+        }
+
+        /**
+         * get the current location (without search or hash part of the URL),
+         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
+         *
+         * @name   Helper.baseUri
+         * @function
+         * @return {string}
+         */
+        me.baseUri = function()
+        {
+            // check for cached version
+            if (baseUri !== null) {
+                return baseUri;
+            }
+
+            baseUri = window.location.origin + window.location.pathname;
+            return baseUri;
+        }
 
         /**
-         * Convert all applicable characters to HTML entities.
-         * From: https://github.com/janl/mustache.js/blob/master/mustache.js#L60
+         * convert all applicable characters to HTML entities
          *
-         * @param string str
-         * @return string escaped HTML
+         * @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
          */
-        htmlEntities: function(str) {
+        me.htmlEntities = function(str) {
             return String(str).replace(
                 /[&<>"'`=\/]/g, function(s) {
-                    return helper.entityMap[s];
+                    return entityMap[s];
                 });
-        },
+        }
 
         /**
-         * character to HTML entity lookup table
+         * resets state, used for unit testing
+         *
+         * @name   Helper.reset
+         * @function
          */
-        entityMap: {
-            '&': '&amp;',
-            '<': '&lt;',
-            '>': '&gt;',
-            '"': '&quot;',
-            "'": '&#39;',
-            '/': '&#x2F;',
-            '`': '&#x60;',
-            '=': '&#x3D;'
+        me.reset = function()
+        {
+            baseUri = null;
         }
-    };
+
+        return me;
+    })();
 
     /**
-     * internationalization methods
+     * internationalization module
+     *
+     * @name I18n
+     * @param  {object} window
+     * @param  {object} document
+     * @class
      */
-    var i18n = {
+    var I18n = (function (window, document) {
+        var me = {};
+
+        /**
+         * const for string of loaded language
+         *
+         * @name I18n.languageLoadedEvent
+         * @private
+         * @prop   {string}
+         * @readonly
+         */
+        var languageLoadedEvent = 'languageLoaded';
+
         /**
          * supported languages, minus the built in 'en'
+         *
+         * @name I18n.supportedLanguages
+         * @private
+         * @prop   {string[]}
+         * @readonly
          */
-        supportedLanguages: ['de', 'fr', 'pl', 'sl', 'zh'],
+        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
 
         /**
-         * translate a string, alias for translate()
+         * built in language
          *
-         * @param string $messageId
-         * @param mixed args one or multiple parameters injected into placeholders
-         * @return string
+         * @name I18n.language
+         * @private
+         * @prop   {string|null}
+         */
+        var language = null;
+
+        /**
+         * translation cache
+         *
+         * @name I18n.translations
+         * @private
+         * @enum   {Object}
+         */
+        var translations = {};
+
+        /**
+         * translate a string, alias for I18n.translate
+         *
+         * @name   I18n._
+         * @function
+         * @param  {jQuery} $element - optional
+         * @param  {string} messageId
+         * @param  {...*} args - one or multiple parameters injected into placeholders
+         * @return {string}
          */
-        _: function()
+        me._ = function()
         {
-            return this.translate(arguments);
-        },
+            return me.translate.apply(this, arguments);
+        }
 
         /**
          * translate a string
          *
-         * @param string $messageId
-         * @param mixed args one or multiple parameters injected into placeholders
-         * @return string
+         * Optionally pass a jQuery element as the first parameter, to automatically
+         * let the text of this element be replaced. In case the (asynchronously
+         * loaded) language is not downloadet yet, this will make sure the string
+         * is replaced when it is actually loaded.
+         * So for easy translations passing the jQuery object to apply it to is
+         * more save, especially when they are loaded in the beginning.
+         *
+         * @name   I18n.translate
+         * @function
+         * @param  {jQuery} $element - optional
+         * @param  {string} messageId
+         * @param  {...*} args - one or multiple parameters injected into placeholders
+         * @return {string}
          */
-        translate: function()
+        me.translate = function()
         {
-            var args = arguments, messageId;
-            if (typeof arguments[0] === 'object')
-            {
-                args = arguments[0];
+            // convert parameters to array
+            var args = Array.prototype.slice.call(arguments),
+                messageId,
+                $element = null;
+
+            // parse arguments
+            if (args[0] instanceof jQuery) {
+                // optional jQuery element as first parameter
+                $element = args[0];
+                args.shift();
             }
+
+            // extract messageId from arguments
             var usesPlurals = $.isArray(args[0]);
-            if (usesPlurals)
-            {
+            if (usesPlurals) {
                 // use the first plural form as messageId, otherwise the singular
                 messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
-            }
-            else
-            {
+            } else {
                 messageId = args[0];
             }
-            if (messageId.length === 0)
-            {
+
+            if (messageId.length === 0) {
                 return messageId;
             }
-            if (!this.translations.hasOwnProperty(messageId))
-            {
-                if (this.language !== 'en')
-                {
-                    console.debug(
-                        'Missing ' + this.language + ' translation for: ' + messageId
-                    );
+
+            // if no translation string cannot be found (in translations object)
+            if (!translations.hasOwnProperty(messageId) || language === null) {
+                // if language is still loading and we have an elemt assigned
+                if (language === null && $element !== null) {
+                    // handle the error by attaching the language loaded event
+                    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);
+                        // re-execute this function
+                        me.translate.apply(this, orgArguments);
+                    });
+
+                    // and fall back to English for now until the real language
+                    // file is loaded
                 }
-                this.translations[messageId] = args[0];
+
+                // for all other langauges than English for which this behaviour
+                // is expected as it is built-in, log error
+                if (language !== null && language !== 'en') {
+                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
+                    // fallback to English
+                }
+
+                // save English translation (should be the same on both sides)
+                translations[messageId] = args[0];
             }
-            if (usesPlurals && $.isArray(this.translations[messageId]))
-            {
+
+            // lookup plural translation
+            if (usesPlurals && $.isArray(translations[messageId])) {
                 var n = parseInt(args[1] || 1, 10),
-                    key = this.getPluralForm(n),
-                    maxKey = this.translations[messageId].length - 1;
-                if (key > maxKey)
-                {
+                    key = me.getPluralForm(n),
+                    maxKey = translations[messageId].length - 1;
+                if (key > maxKey) {
                     key = maxKey;
                 }
-                args[0] = this.translations[messageId][key];
+                args[0] = translations[messageId][key];
                 args[1] = n;
+            } else {
+                // lookup singular translation
+                args[0] = translations[messageId];
             }
-            else
-            {
-                args[0] = this.translations[messageId];
+
+            // format string
+            var output = Helper.sprintf.apply(this, args);
+
+            // if $element is given, apply text to element
+            if ($element !== null) {
+                // get last text node of element
+                var content = $element.contents();
+                if (content.length > 1) {
+                    content[content.length - 1].nodeValue = ' ' + output;
+                } else {
+                    $element.text(output);
+                }
             }
-            return helper.sprintf(args);
-        },
+
+            return output;
+        }
 
         /**
          * per language functions to use to determine the plural form
-         * From: http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
          *
-         * @param int number
-         * @return int array key
+         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
+         * @name   I18n.getPluralForm
+         * @function
+         * @param  {int} n
+         * @return {int} array key
          */
-        getPluralForm: function(n) {
-            switch (this.language)
+        me.getPluralForm = function(n) {
+            switch (language)
             {
                 case 'fr':
+                case 'oc':
                 case 'zh':
                     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);
-                // en, de
+                    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));
+                case 'sl':
+                    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);
             }
-        },
+        }
 
         /**
-         * load translations into cache, then execute callback function
+         * load translations into cache
          *
-         * @param function callback
+         * @name   I18n.loadTranslations
+         * @function
          */
-        loadTranslations: function(callback)
+        me.loadTranslations = function()
         {
-            var selectedLang = helper.getCookie('lang');
-            var language = selectedLang.length > 0 ? selectedLang : (navigator.language || navigator.userLanguage).substring(0, 2);
-            // note that 'en' is built in, so no translation is necessary
-            if (this.supportedLanguages.indexOf(language) === -1)
-            {
-                callback();
+            var newLanguage = Helper.getCookie('lang');
+
+            // auto-select language based on browser settings
+            if (newLanguage.length === 0) {
+                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
             }
-            else
-            {
-                $.getJSON('i18n/' + language + '.json', function(data) {
-                    i18n.language = language;
-                    i18n.translations = data;
-                    callback();
-                });
+
+            // if language is already used skip update
+            if (newLanguage === language) {
+                return;
             }
-        },
 
-        /**
-         * built in language
-         */
-        language: 'en',
+            // if language is built-in (English) skip update
+            if (newLanguage === 'en') {
+                language = 'en';
+                return;
+            }
 
-        /**
-         * translation cache
-         */
-        translations: {}
-    };
+            // if language is not supported, show error
+            if (supportedLanguages.indexOf(newLanguage) === -1) {
+                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
+                language = 'en';
+                return;
+            }
+
+            // load strings from JSON
+            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
+                language = newLanguage;
+                translations = data;
+                $(document).triggerHandler(languageLoadedEvent);
+            }).fail(function (data, textStatus, errorMsg) {
+                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
+                language = 'en';
+            });
+        }
+
+        return me;
+    })(window, document);
 
     /**
-     * filter methods
+     * handles everything related to en/decryption
+     *
+     * @name CryptTool
+     * @class
      */
-    var filter = {
+    var CryptTool = (function () {
+        var me = {};
+
         /**
-         * Compress a message (deflate compression). Returns base64 encoded data.
+         * compress a message (deflate compression), returns base64 encoded data
          *
-         * @param string message
-         * @return base64 string data
+         * @name   CryptTool.compress
+         * @function
+         * @private
+         * @param  {string} message
+         * @return {string} base64 data
          */
-        compress: function(message)
+        function compress(message)
         {
             return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
-        },
+        }
 
         /**
-         * Decompress a message compressed with compress().
+         * decompress a message compressed with cryptToolcompress()
          *
-         * @param base64 string data
-         * @return string message
+         * @name   CryptTool.decompress
+         * @function
+         * @private
+         * @param  {string} data - base64 data
+         * @return {string} message
          */
-        decompress: function(data)
+        function decompress(data)
         {
             return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
-        },
+        }
 
         /**
-         * Compress, then encrypt message with key.
+         * compress, then encrypt message with given key and password
          *
-         * @param string key
-         * @param string password
-         * @param string message
-         * @return encrypted string data
+         * @name   CryptTool.cipher
+         * @function
+         * @param  {string} key
+         * @param  {string} password
+         * @param  {string} message
+         * @return {string} data - JSON with encrypted data
          */
-        cipher: function(key, password, message)
+        me.cipher = function(key, password, message)
         {
             // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
-            var options = {mode: 'gcm', ks: 256, ts: 128};
-            if ((password || '').trim().length === 0)
-            {
-                return sjcl.encrypt(key, this.compress(message), options);
+            var options = {
+                mode: 'gcm',
+                ks: 256,
+                ts: 128
+            };
+
+            if ((password || '').trim().length === 0) {
+                return sjcl.encrypt(key, compress(message), options);
             }
-            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.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.
+         * decrypt message with key, then decompress
          *
-         * @param string key
-         * @param string password
-         * @param encrypted string data
-         * @return string readable message
+         * @name   CryptTool.decipher
+         * @function
+         * @param  {string} key
+         * @param  {string} password
+         * @param  {string} data - JSON with encrypted data
+         * @return {string} decrypted message
          */
-        decipher: function(key, password, data)
+        me.decipher = function(key, password, data)
         {
-            if (data !== undefined)
-            {
-                try
-                {
-                    return this.decompress(sjcl.decrypt(key, data));
-                }
-                catch(err)
-                {
-                    try
-                    {
-                        return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
+            if (data !== undefined) {
+                try {
+                    return decompress(sjcl.decrypt(key, data));
+                } catch(err) {
+                    try {
+                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
+                    } catch(e) {
+                        // ignore error, because ????? @TODO
                     }
-                    catch(e)
-                    {}
                 }
             }
             return '';
         }
-    };
 
-    var privatebin = {
         /**
-         * headers to send in AJAX requests
+         * checks whether the crypt tool has collected enough entropy
+         *
+         * @name   CryptTool.isEntropyReady
+         * @function
+         * @return {bool}
          */
-        headers: {'X-Requested-With': 'JSONHttpRequest'},
+        me.isEntropyReady = function()
+        {
+            return sjcl.random.isReady();
+        }
 
         /**
-         * URL shortners create address
+         * add a listener function, triggered when enough entropy is available
+         *
+         * @name   CryptTool.addEntropySeedListener
+         * @function
+         * @param {function} func
          */
-        shortenerUrl: '',
+        me.addEntropySeedListener = function(func)
+        {
+            sjcl.random.addEventListener('seeded', func);
+        }
 
         /**
-         * URL of newly created paste
+         * returns a random symmetric key
+         *
+         * @name   CryptTool.getSymmetricKey
+         * @function
+         * @return {string} func
          */
-        createdPasteUrl: '',
+        me.getSymmetricKey = function(func)
+        {
+            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
+        }
+
+        return me;
+    })();
+
+    /**
+     * (Model) Data source (aka MVC)
+     *
+     * @name   Model
+     * @class
+     */
+    var Model = (function () {
+        var me = {};
+
+        var $cipherData,
+            $templates;
+
+        var id = null, symmetricKey = null;
 
         /**
-         * Get the current script location (without search or hash part of the URL).
-         * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
+         * returns the expiration set in the HTML
          *
-         * @return string current script location
+         * @name   Model.getExpirationDefault
+         * @function
+         * @return string
+         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
          */
-        scriptLocation: function()
+        me.getExpirationDefault = function()
         {
-            var scriptLocation = window.location.href.substring(0,window.location.href.length
-                - window.location.search.length - window.location.hash.length),
-                hashIndex = scriptLocation.indexOf('#');
-            if (hashIndex !== -1)
-            {
-                scriptLocation = scriptLocation.substring(0, hashIndex);
-            }
-            return scriptLocation;
-        },
+            return $('#pasteExpiration').val();
+        }
 
         /**
-         * Get the pastes unique identifier from the URL
-         * eg. http://server.com/zero/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
+         * returns the format set in the HTML
          *
-         * @return string unique identifier
+         * @name   Model.getFormatDefault
+         * @function
+         * @return string
+         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
          */
-        pasteID: function()
+        me.getFormatDefault = function()
         {
-            return window.location.search.substring(1);
-        },
+            return $('#pasteFormatter').val();
+        }
 
         /**
-         * Return the deciphering key stored in anchor part of the URL
+         * check if cipher data was supplied
          *
-         * @return string key
+         * @name   Model.getCipherData
+         * @function
+         * @return boolean
          */
-        pageKey: function()
+        me.hasCipherData = function()
         {
-            // Some web 2.0 services and redirectors add data AFTER the anchor
-            // (such as &utm_source=...). We will strip any additional data.
-
-            var key = window.location.hash.substring(1),    // Get key
-                i = key.indexOf('=');
-
-            // First, strip everything after the equal sign (=) which signals end of base64 string.
-            if (i > -1)
-            {
-                key = key.substring(0, i + 1);
-            }
-
-            // If the equal sign was not present, some parameters may remain:
-            i = key.indexOf('&');
-            if (i > -1)
-            {
-                key = key.substring(0, i);
-            }
-
-            // Then add trailing equal sign if it's missing
-            if (key.charAt(key.length - 1) !== '=')
-            {
-                key += '=';
-            }
-
-            return key;
-        },
+            return (me.getCipherData().length > 0);
+        }
 
         /**
-         * ask the user for the password and return it
+         * returns the cipher data
          *
-         * @throws error when dialog canceled
-         * @return string password
+         * @name   Model.getCipherData
+         * @function
+         * @return string
          */
-        requestPassword: function()
+        me.getCipherData = function()
         {
-            var password = prompt(i18n._('Please enter the password for this paste:'), '');
-            if (password === null)
-            {
-                throw 'password prompt canceled';
-            }
-            if (password.length === 0)
-            {
-                return this.requestPassword();
-            }
-            return password;
-        },
+            return $cipherData.text();
+        }
 
         /**
-         * use given format on paste, defaults to plain text
+         * get the pastes unique identifier from the URL,
+         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
          *
-         * @param string format
-         * @param string text
+         * @name   Model.getPasteId
+         * @function
+         * @return {string} unique identifier
+         * @throws {string}
          */
-        formatPaste: function(format, text)
+        me.getPasteId = function()
         {
-            helper.setElementText(this.clearText, text);
-            helper.setElementText(this.prettyPrint, text);
-            switch (format || 'plaintext')
-            {
-                case 'markdown':
-                    if (typeof showdown === 'object')
-                    {
-                        showdown.setOption('strikethrough', true);
-                        showdown.setOption('tables', true);
-                        showdown.setOption('tablesHeaderId', true);
-                        var converter = new showdown.Converter();
-                        this.clearText.html(
-                            converter.makeHtml(text)
-                        );
-                        this.clearText.removeClass('hidden');
-                    }
-                    this.prettyMessage.addClass('hidden');
-                    break;
-                case 'syntaxhighlighting':
-                    if (typeof prettyPrintOne === 'function')
-                    {
-                        if (typeof prettyPrint === 'function')
-                        {
-                            prettyPrint();
-                        }
-                        this.prettyPrint.html(
-                            prettyPrintOne(
-                                helper.htmlEntities(text), null, true
-                            )
-                        );
-                    }
-                    // fall through, as the rest is the same
-                default:
-                    // Convert URLs to clickable links.
-                    helper.urls2links(this.clearText);
-                    helper.urls2links(this.prettyPrint);
-                    this.clearText.addClass('hidden');
-                    if (format === 'plaintext')
-                    {
-                        this.prettyPrint.css('white-space', 'pre-wrap');
-                        this.prettyPrint.css('word-break', 'normal');
-                        this.prettyPrint.removeClass('prettyprint');
-                    }
-                    this.prettyMessage.removeClass('hidden');
+            if (id === null) {
+                id = window.location.search.substring(1);
+
+                if (id === '') {
+                    throw 'no paste id given';
+                }
             }
-        },
+
+            return id;
+        }
 
         /**
-         * Show decrypted text in the display area, including discussion (if open)
+         * return the deciphering key stored in anchor part of the URL
          *
-         * @param string key : decryption key
-         * @param object paste : paste object including comments to display (items = array with keys ('data','meta')
+         * @name   Model.getPasteKey
+         * @function
+         * @return {string|null} key
+         * @throws {string}
          */
-        displayMessages: function(key, paste)
+        me.getPasteKey = function()
         {
-            // Try to decrypt the paste.
-            var password = this.passwordInput.val();
-            if (!this.prettyPrint.hasClass('prettyprinted')) {
-                try
-                {
-                    if (paste.attachment)
-                    {
-                        var attachment = filter.decipher(key, password, paste.attachment);
-                        if (attachment.length === 0)
-                        {
-                            if (password.length === 0)
-                            {
-                                password = this.requestPassword();
-                            }
-                            attachment = filter.decipher(key, password, paste.attachment);
-                        }
-                        if (attachment.length === 0)
-                        {
-                            throw 'failed to decipher attachment';
-                        }
-
-                        if (paste.attachmentname)
-                        {
-                            var attachmentname = filter.decipher(key, password, paste.attachmentname);
-                            if (attachmentname.length > 0)
-                            {
-                                this.attachmentLink.attr('download', attachmentname);
-                            }
-                        }
-                        this.attachmentLink.attr('href', attachment);
-                        this.attachment.removeClass('hidden');
-
-                        // if the attachment is an image, display it
-                        var imagePrefix = 'data:image/';
-                        if (attachment.substring(0, imagePrefix.length) === imagePrefix)
-                        {
-                            this.image.html(
-                                $(document.createElement('img'))
-                                    .attr('src', attachment)
-                                    .attr('class', 'img-thumbnail')
-                            );
-                            this.image.removeClass('hidden');
-                        }
-                    }
-                    var cleartext = filter.decipher(key, password, paste.data);
-                    if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
-                    {
-                        password = this.requestPassword();
-                        cleartext = filter.decipher(key, password, paste.data);
-                    }
-                    if (cleartext.length === 0 && !paste.attachment)
-                    {
-                        throw 'failed to decipher message';
-                    }
+            if (symmetricKey === null) {
+                symmetricKey = window.location.hash.substring(1);
 
-                    this.passwordInput.val(password);
-                    if (cleartext.length > 0)
-                    {
-                        $('#pasteFormatter').val(paste.meta.formatter);
-                        this.formatPaste(paste.meta.formatter, cleartext);
-                    }
+                if (symmetricKey === '') {
+                    throw 'no encryption key given';
                 }
-                catch(err)
+
+                // Some web 2.0 services and redirectors add data AFTER the anchor
+                // (such as &utm_source=...). We will strip any additional data.
+                var ampersandPos = symmetricKey.indexOf('&');
+                if (ampersandPos > -1)
                 {
-                    this.clearText.addClass('hidden');
-                    this.prettyMessage.addClass('hidden');
-                    this.cloneButton.addClass('hidden');
-                    this.showError(i18n._('Could not decrypt data (Wrong key?)'));
-                    return;
+                    symmetricKey = symmetricKey.substring(0, ampersandPos);
                 }
             }
 
-            // Display paste expiration / for your eyes only.
-            if (paste.meta.expire_date)
-            {
-                var expiration = helper.secondsToHuman(paste.meta.remaining_time),
-                    expirationLabel = [
-                        'This document will expire in %d ' + expiration[1] + '.',
-                        'This document will expire in %d ' + expiration[1] + 's.'
-                    ];
-                helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0]));
-                this.remainingTime.removeClass('foryoureyesonly')
-                                  .removeClass('hidden');
-            }
-            if (paste.meta.burnafterreading)
-            {
-                // unfortunately many web servers don't support DELETE (and PUT) out of the box
-                $.ajax({
-                    type: 'POST',
-                    url: this.scriptLocation() + '?' + this.pasteID(),
-                    data: {deletetoken: 'burnafterreading'},
-                    dataType: 'json',
-                    headers: this.headers
-                })
-                .fail(function() {
-                    privatebin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
-                });
-                helper.setMessage(this.remainingTime, i18n._(
-                    'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
-                ));
-                this.remainingTime.addClass('foryoureyesonly')
-                                  .removeClass('hidden');
-                // Discourage cloning (as it can't really be prevented).
-                this.cloneButton.addClass('hidden');
-            }
-
-            // If the discussion is opened on this paste, display it.
-            if (paste.meta.opendiscussion)
-            {
-                this.comments.html('');
-
-                // iterate over comments
-                for (var i = 0; i < paste.comments.length; ++i)
-                {
-                    var place = this.comments;
-                    var comment = paste.comments[i];
-                    var commenttext = filter.decipher(key, password, comment.data);
-                    // If parent comment exists, display below (CSS will automatically shift it right.)
-                    var cname = '#comment_' + comment.parentid;
-
-                    // If the element exists in page
-                    if ($(cname).length)
-                    {
-                        place = $(cname);
-                    }
-                    var divComment = $('<article><div class="comment" id="comment_' + comment.id + '">'
-                                   + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
-                                   + '<button class="btn btn-default btn-sm">' + i18n._('Reply') + '</button>'
-                                   + '</div></article>');
-                    divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this));
-                    helper.setElementText(divComment.find('div.commentdata'), commenttext);
-                    // Convert URLs to clickable links in comment.
-                    helper.urls2links(divComment.find('div.commentdata'));
-
-                    // Try to get optional nickname:
-                    var nick = filter.decipher(key, password, comment.meta.nickname);
-                    if (nick.length > 0)
-                    {
-                        divComment.find('span.nickname').text(nick);
-                    }
-                    else
-                    {
-                        divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
-                    }
-                    divComment.find('span.commentdate')
-                              .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
-                              .attr('title', 'CommentID: ' + comment.id);
-
-                    // If an avatar is available, display it.
-                    if (comment.meta.vizhash)
-                    {
-                        divComment.find('span.nickname')
-                                  .before(
-                                    '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
-                                    i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
-                                  );
-                    }
-
-                    place.append(divComment);
-                }
-                var divComment = $(
-                    '<div class="comment"><button class="btn btn-default btn-sm">' +
-                    i18n._('Add comment') + '</button></div>'
-                );
-                divComment.find('button').click({commentid: this.pasteID()}, $.proxy(this.openReply, this));
-                this.comments.append(divComment);
-                this.discussion.removeClass('hidden');
-            }
-        },
+            return symmetricKey;
+        }
 
         /**
-         * Open the comment entry when clicking the "Reply" button of a comment.
+         * returns a jQuery copy of the HTML template
          *
-         * @param Event event
+         * @name Model.getTemplate
+         * @function
+         * @param  {string} name - the name of the template
+         * @return {jQuery}
          */
-        openReply: function(event)
+        me.getTemplate = function(name)
         {
-            event.preventDefault();
-            var source = $(event.target),
-                commentid = event.data.commentid,
-                hint = i18n._('Optional nickname...');
-
-            // Remove any other reply area.
-            $('div.reply').remove();
-            var reply = $(
-                '<div class="reply">' +
-                '<input type="text" id="nickname" class="form-control" title="' + hint + '" placeholder="' + hint + '" />' +
-                '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>' +
-                '<br /><div id="replystatus"></div><button id="replybutton" class="btn btn-default btn-sm">' +
-                i18n._('Post comment') + '</button></div>'
-            );
-            reply.find('button').click({parentid: commentid}, $.proxy(this.sendComment, this));
-            source.after(reply);
-            this.replyStatus = $('#replystatus');
-            $('#replymessage').focus();
-        },
+            // find template
+            var $element = $templates.find('#' + name + 'template').clone(true);
+            // change ID to avoid collisions (one ID should really be unique)
+            return $element.prop('id', name);
+        }
 
         /**
-         * Send a reply in a discussion.
+         * resets state, used for unit testing
          *
-         * @param Event event
+         * @name   Model.reset
+         * @function
          */
-        sendComment: function(event)
+        me.reset = function()
         {
-            event.preventDefault();
-            this.errorMessage.addClass('hidden');
-            // Do not send if no data.
-            var replyMessage = $('#replymessage');
-            if (replyMessage.val().length === 0)
-            {
-                return;
-            }
-
-            this.showStatus(i18n._('Sending comment...'), true);
-            var parentid = event.data.parentid;
-            var cipherdata = filter.cipher(this.pageKey(), this.passwordInput.val(), replyMessage.val());
-            var ciphernickname = '';
-            var nick = $('#nickname').val();
-            if (nick !== '')
-            {
-                ciphernickname = filter.cipher(this.pageKey(), this.passwordInput.val(), nick);
-            }
-            var data_to_send = {
-                data:     cipherdata,
-                parentid: parentid,
-                pasteid:  this.pasteID(),
-                nickname: ciphernickname
-            };
+            $cipherData = $templates = id = symmetricKey = null;
+        }
 
-            $.ajax({
-                type: 'POST',
-                url: this.scriptLocation(),
-                data: data_to_send,
-                dataType: 'json',
-                headers: this.headers,
-                success: function(data)
-                {
-                    if (data.status === 0)
-                    {
-                        privatebin.showStatus(i18n._('Comment posted.'), false);
-                        $.ajax({
-                            type: 'GET',
-                            url: privatebin.scriptLocation() + '?' + privatebin.pasteID(),
-                            dataType: 'json',
-                            headers: privatebin.headers,
-                            success: function(data)
-                            {
-                                if (data.status === 0)
-                                {
-                                    privatebin.displayMessages(privatebin.pageKey(), data);
-                                }
-                                else if (data.status === 1)
-                                {
-                                    privatebin.showError(i18n._('Could not refresh display: %s', data.message));
-                                }
-                                else
-                                {
-                                    privatebin.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
-                                }
-                            }
-                        })
-                        .fail(function() {
-                            privatebin.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
-                        });
-                    }
-                    else if (data.status === 1)
-                    {
-                        privatebin.showError(i18n._('Could not post comment: %s', data.message));
-                    }
-                    else
-                    {
-                        privatebin.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
-                    }
-                }
-            })
-            .fail(function() {
-                privatebin.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
-            });
-        },
 
         /**
-         * Send a new paste to server
+         * init navigation manager
          *
-         * @param Event event
+         * preloads jQuery elements
+         *
+         * @name   Model.init
+         * @function
          */
-        sendData: function(event)
+        me.init = function()
         {
-            event.preventDefault();
-            var file = document.getElementById('file'),
-                files = (file && file.files) ? file.files : null; // FileList object
-
-            // Do not send if no data.
-            if (this.message.val().length === 0 && !(files && files[0]))
-            {
-                return;
-            }
-
-            // If sjcl has not collected enough entropy yet, display a message.
-            if (!sjcl.random.isReady())
-            {
-                this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
-                sjcl.random.addEventListener('seeded', function() {
-                    this.sendData(event);
-                });
-                return;
-            }
+            $cipherData = $('#cipherdata');
+            $templates = $('#templates');
+        }
 
-            $('.navbar-toggle').click();
-            this.password.addClass('hidden');
-            this.showStatus(i18n._('Sending paste...'), true);
+        return me;
+    })();
 
-            var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
-            var password = this.passwordInput.val();
-            if(files && files[0])
-            {
-                if(typeof FileReader === undefined)
-                {
-                    this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
-                    return;
-                }
-                var reader = new FileReader();
-                // Closure to capture the file information.
-                reader.onload = (function(theFile)
-                {
-                    return function(e) {
-                        privatebin.sendDataContinue(
-                            randomkey,
-                            filter.cipher(randomkey, password, e.target.result),
-                            filter.cipher(randomkey, password, theFile.name)
-                        );
-                    };
-                })(files[0]);
-                reader.readAsDataURL(files[0]);
-            }
-            else if(this.attachmentLink.attr('href'))
-            {
-                this.sendDataContinue(
-                    randomkey,
-                    filter.cipher(randomkey, password, this.attachmentLink.attr('href')),
-                    this.attachmentLink.attr('download')
-                );
-            }
-            else
-            {
-                this.sendDataContinue(randomkey, '', '');
-            }
-        },
+    /**
+     * Helper functions for user interface
+     *
+     * everything directly UI-related, which fits nowhere else
+     *
+     * @name   UiHelper
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var UiHelper = (function (window, document) {
+        var me = {};
 
         /**
-         * Send a new paste to server, step 2
+         * handle history (pop) state changes
          *
-         * @param string randomkey
-         * @param encrypted string cipherdata_attachment
-         * @param encrypted string cipherdata_attachment_name
+         * currently this does only handle redirects to the home page.
+         *
+         * @name   UiHelper.historyChange
+         * @private
+         * @function
+         * @param  {Event} event
          */
-        sendDataContinue: function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
+        function historyChange(event)
         {
-            var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val());
-            var data_to_send = {
-                data:             cipherdata,
-                expire:           $('#pasteExpiration').val(),
-                formatter:        $('#pasteFormatter').val(),
-                burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
-                opendiscussion:   this.openDiscussion.is(':checked') ? 1 : 0
-            };
-            if (cipherdata_attachment.length > 0)
-            {
-                data_to_send.attachment = cipherdata_attachment;
-                if (cipherdata_attachment_name.length > 0)
-                {
-                    data_to_send.attachmentname = cipherdata_attachment_name;
-                }
+            var currentLocation = Helper.baseUri();
+            if (event.originalEvent.state === null && // no state object passed
+                event.originalEvent.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
+                window.location.href = currentLocation;
             }
-            $.ajax({
-                type: 'POST',
-                url: this.scriptLocation(),
-                data: data_to_send,
-                dataType: 'json',
-                headers: this.headers,
-                success: function(data)
-                {
-                    if (data.status === 0) {
-                        privatebin.stateExistingPaste();
-                        var url = privatebin.scriptLocation() + '?' + data.id + '#' + randomkey;
-                        var deleteUrl = privatebin.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
-                        privatebin.showStatus('', false);
-                        privatebin.errorMessage.addClass('hidden');
-
-                        $('#pastelink').html(
-                            i18n._(
-                                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
-                                url, url
-                            ) + privatebin.shortenUrl(url)
-                        );
-                        var shortenButton = $('#shortenbutton');
-                        if (shortenButton) {
-                            shortenButton.click($.proxy(privatebin.sendToShortener, privatebin));
-                        }
-                        $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
-                        privatebin.pasteResult.removeClass('hidden');
-                        // We pre-select the link so that the user only has to [Ctrl]+[c] the link.
-                        helper.selectText('pasteurl');
-                        privatebin.showStatus('', false);
-                        privatebin.formatPaste(data_to_send.formatter, privatebin.message.val());
-                    }
-                    else if (data.status === 1)
-                    {
-                        privatebin.showError(i18n._('Could not create paste: %s', data.message));
-                    }
-                    else
-                    {
-                        privatebin.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
-                    }
-                }
-            })
-            .fail(function()
-            {
-                privatebin.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
-            });
-        },
+        }
 
         /**
-         * Check if a URL shortener was defined and create HTML containing a link to it.
+         * reload the page
+         *
+         * This takes the user to the PrivateBin homepage.
          *
-         * @param string url
-         * @return string html
+         * @name   UiHelper.reloadHome
+         * @function
          */
-        shortenUrl: function(url)
+        me.reloadHome = function()
         {
-            var shortenerHtml = $('#shortenbutton');
-            if (shortenerHtml) {
-                this.shortenerUrl = shortenerHtml.data('shortener');
-                this.createdPasteUrl = url;
-                return ' ' + $('<div />').append(shortenerHtml.clone()).html();
-            }
-            return '';
-        },
+            window.location.href = Helper.baseUri();
+        }
 
         /**
-         * Put the screen in "New paste" mode.
+         * checks whether the element is currently visible in the viewport (so
+         * the user can actually see it)
+         *
+         * @see    {@link https://stackoverflow.com/a/40658647}
+         * @name   UiHelper.isVisible
+         * @function
+         * @param  {jQuery} $element The link hash to move to.
          */
-        stateNewPaste: function()
+        me.isVisible = function($element)
         {
-            this.message.text('');
-            this.attachment.addClass('hidden');
-            this.cloneButton.addClass('hidden');
-            this.rawTextButton.addClass('hidden');
-            this.remainingTime.addClass('hidden');
-            this.pasteResult.addClass('hidden');
-            this.clearText.addClass('hidden');
-            this.discussion.addClass('hidden');
-            this.prettyMessage.addClass('hidden');
-            this.sendButton.removeClass('hidden');
-            this.expiration.removeClass('hidden');
-            this.formatter.removeClass('hidden');
-            this.burnAfterReadingOption.removeClass('hidden');
-            this.openDisc.removeClass('hidden');
-            this.newButton.removeClass('hidden');
-            this.password.removeClass('hidden');
-            this.attach.removeClass('hidden');
-            this.message.removeClass('hidden');
-            this.preview.removeClass('hidden');
-            this.message.focus();
-        },
+            var elementTop = $element.offset().top;
+            var elementBottom = elementTop + $element.outerHeight();
+
+            var viewportTop = $(window).scrollTop();
+            var viewportBottom = viewportTop + $(window).height();
+
+            return (elementTop > viewportTop && elementTop < viewportBottom);
+        }
 
         /**
-         * Put the screen in "Existing paste" mode.
+         * scrolls to a specific element
          *
-         * @param boolean preview (optional) : tell if the preview tabs should be displayed, defaults to false.
+         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
+         * @name   UiHelper.scrollTo
+         * @function
+         * @param  {jQuery}           $element        The link hash to move to.
+         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
+         * @param  {string}           animationEffect   passed to jQuery .animate
+         * @param  {function}         finishedCallback  function to call after animation finished
          */
-        stateExistingPaste: function(preview)
+        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
         {
-            preview = preview || false;
+            var $body = $('html, body'),
+                margin = 50,
+                callbackCalled = false;
+
+            //calculate destination place
+            var dest = 0;
+            // if it would scroll out of the screen at the bottom only scroll it as
+            // far as the screen can go
+            if ($element.offset().top > $(document).height() - $(window).height()) {
+                dest = $(document).height() - $(window).height();
+            } else {
+                dest = $element.offset().top - margin;
+            }
+            // skip animation if duration is set to 0
+            if (animationDuration === 0) {
+                window.scrollTo(0, dest);
+            } else {
+                // stop previous animation
+                $body.stop();
+                // scroll to destination
+                $body.animate({
+                    scrollTop: dest
+                }, animationDuration, animationEffect);
+            }
+
+            // as we have finished we can enable scrolling again
+            $body.queue(function (next) {
+                if (!callbackCalled) {
+                    // call user function if needed
+                    if (typeof finishedCallback !== 'undefined') {
+                        finishedCallback();
+                    }
 
-            if (!preview)
-            {
-                // No "clone" for IE<10.
-                if ($('#oldienotice').is(":visible"))
-                {
-                    this.cloneButton.addClass('hidden');
-                }
-                else
-                {
-                    this.cloneButton.removeClass('hidden');
+                    // prevent calling this function twice
+                    callbackCalled = true;
                 }
-
-                this.rawTextButton.removeClass('hidden');
-                this.sendButton.addClass('hidden');
-                this.attach.addClass('hidden');
-                this.expiration.addClass('hidden');
-                this.formatter.addClass('hidden');
-                this.burnAfterReadingOption.addClass('hidden');
-                this.openDisc.addClass('hidden');
-                this.newButton.removeClass('hidden');
-                this.preview.addClass('hidden');
-            }
-
-            this.pasteResult.addClass('hidden');
-            this.message.addClass('hidden');
-            this.clearText.addClass('hidden');
-            this.prettyMessage.addClass('hidden');
-        },
+                next();
+            });
+        }
 
         /**
-         * If "burn after reading" is checked, disable discussion.
+         * initialize
+         *
+         * @name   UiHelper.init
+         * @function
          */
-        changeBurnAfterReading: function()
+        me.init = function()
         {
-            if (this.burnAfterReading.is(':checked') )
-            {
-                this.openDisc.addClass('buttondisabled');
-                this.openDiscussion.attr({checked: false, disabled: true});
-            }
-            else
-            {
-                this.openDisc.removeClass('buttondisabled');
-                this.openDiscussion.removeAttr('disabled');
-            }
-        },
+            // update link to home page
+            $('.reloadlink').prop('href', Helper.baseUri());
+
+            $(window).on('popstate', historyChange);
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * Alert/error manager
+     *
+     * @name   Alert
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var Alert = (function (window, document) {
+        var me = {};
+
+        var $errorMessage,
+            $loadingIndicator,
+            $statusMessage,
+            $remainingTime;
+
+        var currentIcon = [
+            'glyphicon-time', // loading icon
+            'glyphicon-info-sign', // status icon
+            '', // resevered for warning, not used yet
+            'glyphicon-alert' // error icon
+        ];
+
+        var alertType = [
+            'loading', // not in bootstrap, but using a good value here
+            'info', // status icon
+            'warning', // not used yet
+            'danger' // error icon
+        ];
+
+        var customHandler;
 
         /**
-         * If discussion is checked, disable "burn after reading".
+         * forwards a request to the i18n module and shows the element
+         *
+         * @name   Alert.handleNotification
+         * @private
+         * @function
+         * @param  {int} id - id of notification
+         * @param  {jQuery} $element - jQuery object
+         * @param  {string|array} args
+         * @param  {string|null} icon - optional, icon
          */
-        changeOpenDisc: function()
+        function handleNotification(id, $element, args, icon)
         {
-            if (this.openDiscussion.is(':checked') )
-            {
-                this.burnAfterReadingOption.addClass('buttondisabled');
-                this.burnAfterReading.attr({checked: false, disabled: true});
+            // basic parsing/conversion of parameters
+            if (typeof icon === 'undefined') {
+                icon = null;
+            }
+            if (typeof args === 'undefined') {
+                args = null;
+            } else if (typeof args === 'string') {
+                // convert string to array if needed
+                args = [args];
+            }
+
+            // pass to custom handler if defined
+            if (typeof customHandler === 'function') {
+                var handlerResult = customHandler(alertType[id], $element, args, icon);
+                if (handlerResult === true) {
+                    // if it returs true, skip own handler
+                    return;
+                }
+                if (handlerResult instanceof jQuery) {
+                    // continue processing with new element
+                    $element = handlerResult;
+                    icon = null; // icons not supported in this case
+                }
             }
-            else
-            {
-                this.burnAfterReadingOption.removeClass('buttondisabled');
-                this.burnAfterReading.removeAttr('disabled');
+
+            // handle icon
+            if (icon !== null && // icon was passed
+                icon !== currentIcon[id] // and it differs from current icon
+            ) {
+                var $glyphIcon = $element.find(':first');
+
+                // remove (previous) icon
+                $glyphIcon.removeClass(currentIcon[id]);
+
+                // any other thing as a string (e.g. 'null') (only) removes the icon
+                if (typeof icon === 'string') {
+                    // set new icon
+                    currentIcon[id] = 'glyphicon-' + icon;
+                    $glyphIcon.addClass(currentIcon[id]);
+                }
             }
-        },
+
+            // show text
+            if (args !== null) {
+                // add jQuery object to it as first parameter
+                args.unshift($element);
+                // pass it to I18n
+                I18n._.apply(this, args);
+            }
+
+            // show notification
+            $element.removeClass('hidden');
+        }
 
         /**
-         * Forward to URL shortener.
+         * display a status message
          *
-         * @param Event event
+         * This automatically passes the text to I18n for translation.
+         *
+         * @name   Alert.showStatus
+         * @function
+         * @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
          */
-        sendToShortener: function(event)
+        me.showStatus = function(message, icon, dismissable, autoclose)
         {
-            event.preventDefault();
-            window.location.href = this.shortenerUrl + encodeURIComponent(this.createdPasteUrl);
-        },
+            console.log('status shown: ', message);
+            // @TODO: implement dismissable
+            // @TODO: implement autoclose
+
+            handleNotification(1, $statusMessage, message, icon);
+        }
 
         /**
-         * Reload the page.
+         * display an error message
          *
-         * @param Event event
+         * This automatically passes the text to I18n for translation.
+         *
+         * @name   Alert.showError
+         * @function
+         * @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
          */
-        reloadPage: function(event)
+        me.showError = function(message, icon, dismissable, autoclose)
         {
-            event.preventDefault();
-            window.location.href = this.scriptLocation();
-        },
+            console.error('error message shown: ', message);
+            // @TODO: implement dismissable (bootstrap add-on has it)
+            // @TODO: implement autoclose
+
+            handleNotification(3, $errorMessage, message, icon);
+        }
 
         /**
-         * Return raw text.
+         * display remaining message
          *
-         * @param Event event
+         * This automatically passes the text to I18n for translation.
+         *
+         * @name   Alert.showRemaining
+         * @function
+         * @param  {string|array} message     string, use an array for %s/%d options
          */
-        rawText: function(event)
+        me.showRemaining = function(message)
         {
-            event.preventDefault();
-            var paste = $('#pasteFormatter').val() === 'markdown' ?
-                this.prettyPrint.text() : this.clearText.text();
-            history.pushState(
-                null, document.title, this.scriptLocation() + '?' +
-                this.pasteID() + '#' + this.pageKey()
-            );
-            // we use text/html instead of text/plain to avoid a bug when
-            // reloading the raw text view (it reverts to type text/html)
-            var newDoc = document.open('text/html', 'replace');
-            newDoc.write('<pre>' + paste + '</pre>');
-            newDoc.close();
-        },
+            console.log('remaining message shown: ', message);
+            handleNotification(1, $remainingTime, message);
+        }
 
         /**
-         * Clone the current paste.
+         * shows a loading message, optionally with a percentage
+         *
+         * This automatically passes all texts to the i10s module.
          *
-         * @param Event event
+         * @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
          */
-        clonePaste: function(event)
+        me.showLoading = function(message, percentage, icon)
         {
-            event.preventDefault();
-            this.stateNewPaste();
-
-            // Erase the id and the key in url
-            history.replaceState(null, document.title, this.scriptLocation());
+            if (typeof message !== 'undefined' && message !== null) {
+                console.log('status changed: ', message);
+            }
 
-            this.showStatus('', false);
-            if (this.attachmentLink.attr('href'))
-            {
-                this.clonedFile.removeClass('hidden');
-                this.fileWrap.addClass('hidden');
+            // default message text
+            if (typeof message === 'undefined') {
+                message = 'Loading…';
             }
-            this.message.text(
-                $('#pasteFormatter').val() === 'markdown' ?
-                    this.prettyPrint.text() : this.clearText.text()
-            );
-            $('.navbar-toggle').click();
-        },
 
-        /**
-         * Set the expiration on bootstrap templates.
-         *
-         * @param Event event
-         */
-        setExpiration: function(event)
-        {
-            event.preventDefault();
-            var target = $(event.target);
-            $('#pasteExpiration').val(target.data('expiration'));
-            $('#pasteExpirationDisplay').text(target.text());
-        },
+            // currently percentage parameter is ignored
+            // // @TODO handle it here…
+
+            handleNotification(0, $loadingIndicator, message, icon);
+
+            // show loading status (cursor)
+            $('body').addClass('loading');
+        }
 
         /**
-         * Set the format on bootstrap templates.
+         * hides the loading message
          *
-         * @param Event event
+         * @name   Alert.hideLoading
+         * @function
          */
-        setFormat: function(event)
+        me.hideLoading = function()
         {
-            event.preventDefault();
-            var target = $(event.target);
-            $('#pasteFormatter').val(target.data('format'));
-            $('#pasteFormatterDisplay').text(target.text());
+            $loadingIndicator.addClass('hidden');
 
-            if (this.messagePreview.parent().hasClass('active')) {
-                this.viewPreview(event);
-            }
-        },
+            // hide loading cursor
+            $('body').removeClass('loading');
+        }
 
         /**
-         * Set the language on bootstrap templates.
+         * hides any status/error messages
          *
-         * Sets the language cookie and reloads the page.
+         * This does not include the loading message.
          *
-         * @param Event event
+         * @name   Alert.hideMessages
+         * @function
          */
-        setLanguage: function(event)
+        me.hideMessages = function()
         {
-            document.cookie = 'lang=' + $(event.target).data('lang');
-            this.reloadPage(event);
-        },
+            // also possible: $('.statusmessage').addClass('hidden');
+            $statusMessage.addClass('hidden');
+            $errorMessage.addClass('hidden');
+        }
 
         /**
-         * Support input of tab character.
+         * set a custom handler, which gets all notifications.
+         *
+         * This handler gets the following arguments:
+         * alertType (see array), $element, args, icon
+         * If it returns true, the own processing will be stopped so the message
+         * will not be displayed. Otherwise it will continue.
+         * As an aditional feature it can return q jQuery element, which will
+         * then be used to add the message there. Icons are not supported in
+         * that case and will be ignored.
+         * Pass 'null' to reset/delete the custom handler.
+         * Note that there is no notification when a message is supposed to get
+         * hidden.
          *
-         * @param Event event
+         * @name   Alert.setCustomHandler
+         * @function
+         * @param {function|null} newHandler
          */
-        supportTabs: function(event)
+        me.setCustomHandler = function(newHandler)
         {
-            var keyCode = event.keyCode || event.which;
-            // tab was pressed
-            if (keyCode === 9)
-            {
-                // prevent the textarea to lose focus
-                event.preventDefault();
-                // get caret position & selection
-                var val   = this.value,
-                    start = this.selectionStart,
-                    end   = this.selectionEnd;
-                // set textarea value to: text before caret + tab + text after caret
-                this.value = val.substring(0, start) + '\t' + val.substring(end);
-                // put caret at right position again
-                this.selectionStart = this.selectionEnd = start + 1;
-            }
-        },
+            customHandler = newHandler;
+        }
 
         /**
-         * View the editor tab.
+         * init status manager
+         *
+         * preloads jQuery elements
          *
-         * @param Event event
+         * @name   Alert.init
+         * @function
          */
-        viewEditor: function(event)
+        me.init = function()
         {
-            event.preventDefault();
-            this.messagePreview.parent().removeClass('active');
-            this.messageEdit.parent().addClass('active');
-            this.message.focus();
-            this.stateNewPaste();
-        },
+            // hide "no javascript" error message
+            $('#noscript').hide();
+
+            // not a reset, but first set of the elements
+            $errorMessage = $('#errormessage');
+            $loadingIndicator = $('#loadingindicator');
+            $statusMessage = $('#status');
+            $remainingTime = $('#remainingtime');
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * handles paste status/result
+     *
+     * @name   PasteStatus
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var PasteStatus = (function (window, document) {
+        var me = {};
+
+        var $pasteSuccess,
+            $pasteUrl,
+            $remainingTime,
+            $shortenButton;
 
         /**
-         * View the preview tab.
+         * forward to URL shortener
          *
-         * @param Event event
+         * @name   PasteStatus.sendToShortener
+         * @private
+         * @function
+         * @param  {Event} event
          */
-        viewPreview: function(event)
+        function sendToShortener(event)
         {
-            event.preventDefault();
-            this.messageEdit.parent().removeClass('active');
-            this.messagePreview.parent().addClass('active');
-            this.message.focus();
-            this.stateExistingPaste(true);
-            this.formatPaste($('#pasteFormatter').val(), this.message.val());
-        },
+            window.location.href = $shortenButton.data('shortener')
+                                   + encodeURIComponent($pasteUrl.attr('href'));
+        }
 
         /**
-         * Create a new paste.
+         * Forces opening the paste if the link does not do this automatically.
+         *
+         * This is necessary as browsers will not reload the page when it is
+         * already loaded (which is fake as it is set via history.pushState()).
+         *
+         * @name   PasteStatus.pasteLinkClick
+         * @function
+         * @param  {Event} event
          */
-        newPaste: function()
+        function pasteLinkClick(event)
         {
-            this.stateNewPaste();
-            this.showStatus('', false);
-            this.message.text('');
-            this.changeBurnAfterReading();
-            this.changeOpenDisc();
-        },
+            // check if location is (already) shown in URL bar
+            if (window.location.href === $pasteUrl.attr('href')) {
+                // if so we need to load link by reloading the current site
+                window.location.reload(true);
+            }
+        }
 
         /**
-         * Removes an attachment.
+         * creates a notification after a successfull paste upload
+         *
+         * @name   PasteStatus.createPasteNotification
+         * @function
+         * @param  {string} url
+         * @param  {string} deleteUrl
          */
-        removeAttachment: function()
+        me.createPasteNotification = function(url, deleteUrl)
         {
-            this.clonedFile.addClass('hidden');
-            // removes the saved decrypted file data
-            this.attachmentLink.attr('href', '');
-            // the only way to deselect the file is to recreate the input
-            this.fileWrap.html(this.fileWrap.html());
-            this.fileWrap.removeClass('hidden');
-        },
+            $('#pastelink').html(
+                I18n._(
+                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
+                    url, url
+                )
+            );
+            // save newly created element
+            $pasteUrl = $('#pasteurl');
+            // and add click event
+            $pasteUrl.click(pasteLinkClick);
+
+            // shorten button
+            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
+
+            // show result
+            $pasteSuccess.removeClass('hidden');
+            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
+            Helper.selectText($pasteUrl[0]);
+        }
 
         /**
-         * Display an error message
-         * (We use the same function for paste and reply to comments)
+         * shows the remaining time
          *
-         * @param string message : text to display
+         * @name PasteStatus.showRemainingTime
+         * @function
+         * @param {object} pasteMetaData
          */
-        showError: function(message)
+        me.showRemainingTime = function(pasteMetaData)
         {
-            if (this.status.length)
-            {
-                this.status.addClass('errorMessage').text(message);
-            }
-            else
-            {
-                this.errorMessage.removeClass('hidden');
-                helper.setMessage(this.errorMessage, message);
-            }
-            if (typeof this.replyStatus !== 'undefined') {
-                this.replyStatus.addClass('errorMessage');
-                this.replyStatus.addClass(this.errorMessage.attr('class'));
-                if (this.status.length)
-                {
-                    this.replyStatus.html(this.status.html());
-                }
-                else
-                {
-                    this.replyStatus.html(this.errorMessage.html());
-                }
+            if (pasteMetaData.burnafterreading) {
+                // display paste "for your eyes only" if it is deleted
+
+                // actually remove paste, before we claim it is deleted
+                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
+
+                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
+                $remainingTime.addClass('foryoureyesonly');
+
+                // discourage cloning (it cannot really be prevented)
+                TopNav.hideCloneButton();
+
+            } else if (pasteMetaData.expire_date) {
+                // display paste expiration
+                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
+                    expirationLabel = [
+                        'This document will expire in %d ' + expiration[1] + '.',
+                        'This document will expire in %d ' + expiration[1] + 's.'
+                    ];
+
+                Alert.showRemaining([expirationLabel, expiration[0]]);
+                $remainingTime.removeClass('foryoureyesonly')
+            } else {
+                // never expires
+                return;
             }
-        },
+
+            // in the end, display notification
+            $remainingTime.removeClass('hidden');
+        }
 
         /**
-         * Display a status message
-         * (We use the same function for paste and reply to comments)
+         * hides the remaining time and successful upload notification
          *
-         * @param string message : text to display
-         * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
+         * @name PasteStatus.hideRemainingTime
+         * @function
          */
-        showStatus: function(message, spin)
+        me.hideMessages = function()
         {
-            if (typeof this.replyStatus !== 'undefined') {
-                this.replyStatus.removeClass('errorMessage').text(message);
-            }
-            if (!message)
-            {
-                this.status.html(' ');
-                return;
-            }
-            if (message === '')
-            {
-                this.status.html(' ');
-                return;
-            }
-            this.status.removeClass('errorMessage').text(message);
-            if (spin)
-            {
-                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
-                this.status.prepend(img);
-                if (typeof this.replyStatus !== 'undefined') {
-                    this.replyStatus.prepend(img);
-                }
-            }
-        },
+            $remainingTime.addClass('hidden');
+            $pasteSuccess.addClass('hidden');
+        }
 
         /**
-         * bind events to DOM elements
+         * init status manager
+         *
+         * preloads jQuery elements
+         *
+         * @name   PasteStatus.init
+         * @function
          */
-        bindEvents: function()
+        me.init = function()
         {
-            this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
-            this.openDisc.change($.proxy(this.changeOpenDisc, this));
-            this.sendButton.click($.proxy(this.sendData, this));
-            this.cloneButton.click($.proxy(this.clonePaste, this));
-            this.rawTextButton.click($.proxy(this.rawText, this));
-            this.fileRemoveButton.click($.proxy(this.removeAttachment, this));
-            $('.reloadlink').click($.proxy(this.reloadPage, this));
-            this.message.keydown(this.supportTabs);
-            this.messageEdit.click($.proxy(this.viewEditor, this));
-            this.messagePreview.click($.proxy(this.viewPreview, this));
+            $pasteSuccess = $('#pasteSuccess');
+            // $pasteUrl is saved in me.createPasteNotification() after creation
+            $remainingTime = $('#remainingtime');
+            $shortenButton = $('#shortenbutton');
 
-            // bootstrap template drop downs
-            $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(this.setExpiration, this));
-            $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(this.setFormat, this));
-            $('#language ul.dropdown-menu li a').click($.proxy(this.setLanguage, this));
+            // bind elements
+            $shortenButton.click(sendToShortener);
+        }
 
-            // page template drop down
-            $('#language select option').click($.proxy(this.setLanguage, this));
-        },
+        return me;
+    })(window, document);
+
+    /**
+     * password prompt
+     *
+     * @name Prompt
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var Prompt = (function (window, document) {
+        var me = {};
+
+        var $passwordDecrypt,
+            $passwordForm,
+            $passwordModal;
+
+        var password = '';
 
         /**
-         * main application
+         * submit a password in the modal dialog
+         *
+         * @name Prompt.submitPasswordModal
+         * @private
+         * @function
+         * @param  {Event} event
          */
-        init: function()
+        function submitPasswordModal(event)
         {
-            // hide "no javascript" message
-            $('#noscript').hide();
+            event.preventDefault();
 
-            // preload jQuery wrapped DOM elements and bind events
-            this.attach = $('#attach');
-            this.attachment = $('#attachment');
-            this.attachmentLink = $('#attachment a');
-            this.burnAfterReading = $('#burnafterreading');
-            this.burnAfterReadingOption = $('#burnafterreadingoption');
-            this.cipherData = $('#cipherdata');
-            this.clearText = $('#cleartext');
-            this.cloneButton = $('#clonebutton');
-            this.clonedFile = $('#clonedfile');
-            this.comments = $('#comments');
-            this.discussion = $('#discussion');
-            this.errorMessage = $('#errormessage');
-            this.expiration = $('#expiration');
-            this.fileRemoveButton = $('#fileremovebutton');
-            this.fileWrap = $('#filewrap');
-            this.formatter = $('#formatter');
-            this.image = $('#image');
-            this.message = $('#message');
-            this.messageEdit = $('#messageedit');
-            this.messagePreview = $('#messagepreview');
-            this.newButton = $('#newbutton');
-            this.openDisc = $('#opendisc');
-            this.openDiscussion = $('#opendiscussion');
-            this.password = $('#password');
-            this.passwordInput = $('#passwordinput');
-            this.pasteResult = $('#pasteresult');
-            this.prettyMessage = $('#prettymessage');
-            this.prettyPrint = $('#prettyprint');
-            this.preview = $('#preview');
-            this.rawTextButton = $('#rawtextbutton');
-            this.remainingTime = $('#remainingtime');
-            this.sendButton = $('#sendbutton');
-            this.status = $('#status');
-            this.bindEvents();
-
-            // Display status returned by php code if any (eg. Paste was properly deleted.)
-            if (this.status.text().length > 0)
-            {
-                this.showStatus(this.status.text(), false);
+            // get input
+            password = $passwordDecrypt.val();
+
+            // hide modal
+            $passwordModal.modal('hide');
+
+            PasteDecrypter.run();
+        }
+
+        /**
+         * ask the user for the password and set it
+         *
+         * @name Prompt.requestPassword
+         * @function
+         */
+        me.requestPassword = function()
+        {
+            // show new bootstrap method (if available)
+            if ($passwordModal.length !== 0) {
+                $passwordModal.modal({
+                    backdrop: 'static',
+                    keyboard: false
+                });
+                return;
+            }
+
+            // fallback to old method for page template
+            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
+            if (newPassword === null) {
+                throw 'password prompt canceled';
+            }
+            if (password.length === 0) {
+                // recursive…
+                return me.requestPassword();
+            }
+
+            password = newPassword;
+        }
+
+        /**
+         * getthe cached password
+         *
+         * If you do not get a password with this function
+         * (returns an empty string), use requestPassword.
+         *
+         * @name   Prompt.getPassword
+         * @function
+         * @return {string}
+         */
+        me.getPassword = function()
+        {
+            return password;
+        }
+
+        /**
+         * init status manager
+         *
+         * preloads jQuery elements
+         *
+         * @name   Prompt.init
+         * @function
+         */
+        me.init = function()
+        {
+            $passwordDecrypt = $('#passworddecrypt');
+            $passwordForm = $('#passwordform');
+            $passwordModal = $('#passwordmodal');
+
+            // bind events
+
+            // focus password input when it is shown
+            $passwordModal.on('shown.bs.Model', function () {
+                $passwordDecrypt.focus();
+            });
+            // handle Model password submission
+            $passwordForm.submit(submitPasswordModal);
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * Manage paste/message input, and preview tab
+     *
+     * Note that the actual preview is handled by PasteViewer.
+     *
+     * @name   Editor
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var Editor = (function (window, document) {
+        var me = {};
+
+        var $editorTabs,
+            $messageEdit,
+            $messagePreview,
+            $message;
+
+        var isPreview = false;
+
+        /**
+         * support input of tab character
+         *
+         * @name   Editor.supportTabs
+         * @function
+         * @param  {Event} event
+         * @this $message (but not used, so it is jQuery-free, possibly faster)
+         */
+        function supportTabs(event)
+        {
+            var keyCode = event.keyCode || event.which;
+            // tab was pressed
+            if (keyCode === 9) {
+                // get caret position & selection
+                var val   = this.value,
+                    start = this.selectionStart,
+                    end   = this.selectionEnd;
+                // set textarea value to: text before caret + tab + text after caret
+                this.value = val.substring(0, start) + '\t' + val.substring(end);
+                // put caret at right position again
+                this.selectionStart = this.selectionEnd = start + 1;
+                // prevent the textarea to lose focus
+                event.preventDefault();
+            }
+        }
+
+        /**
+         * view the Editor tab
+         *
+         * @name   Editor.viewEditor
+         * @function
+         * @param  {Event} event - optional
+         */
+        function viewEditor(event)
+        {
+            // toggle buttons
+            $messageEdit.addClass('active');
+            $messagePreview.removeClass('active');
+
+            PasteViewer.hide();
+
+            // reshow input
+            $message.removeClass('hidden');
+
+            me.focusInput();
+
+            // finish
+            isPreview = false;
+
+            // prevent jumping of page to top
+            if (typeof event !== 'undefined') {
+                event.preventDefault();
+            }
+        }
+
+        /**
+         * view the preview tab
+         *
+         * @name   Editor.viewPreview
+         * @function
+         * @param  {Event} event
+         */
+        function viewPreview(event)
+        {
+            // toggle buttons
+            $messageEdit.removeClass('active');
+            $messagePreview.addClass('active');
+
+            // hide input as now preview is shown
+            $message.addClass('hidden');
+
+            // show preview
+            PasteViewer.setText($message.val());
+            PasteViewer.run();
+
+            // finish
+            isPreview = true;
+
+            // prevent jumping of page to top
+            if (typeof event !== 'undefined') {
+                event.preventDefault();
+            }
+        }
+
+        /**
+         * get the state of the preview
+         *
+         * @name   Editor.isPreview
+         * @function
+         */
+        me.isPreview = function()
+        {
+            return isPreview;
+        }
+
+        /**
+         * reset the Editor view
+         *
+         * @name   Editor.resetInput
+         * @function
+         */
+        me.resetInput = function()
+        {
+            // go back to input
+            if (isPreview) {
+                viewEditor();
+            }
+
+            // clear content
+            $message.val('');
+        }
+
+        /**
+         * shows the Editor
+         *
+         * @name   Editor.show
+         * @function
+         */
+        me.show = function()
+        {
+            $message.removeClass('hidden');
+            $editorTabs.removeClass('hidden');
+        }
+
+        /**
+         * hides the Editor
+         *
+         * @name   Editor.reset
+         * @function
+         */
+        me.hide = function()
+        {
+            $message.addClass('hidden');
+            $editorTabs.addClass('hidden');
+        }
+
+        /**
+         * focuses the message input
+         *
+         * @name   Editor.focusInput
+         * @function
+         */
+        me.focusInput = function()
+        {
+            $message.focus();
+        }
+
+        /**
+         * sets a new text
+         *
+         * @name   Editor.setText
+         * @function
+         * @param {string} newText
+         */
+        me.setText = function(newText)
+        {
+            $message.val(newText);
+        }
+
+        /**
+         * returns the current text
+         *
+         * @name   Editor.getText
+         * @function
+         * @return {string}
+         */
+        me.getText = function()
+        {
+            return $message.val()
+        }
+
+        /**
+         * init status manager
+         *
+         * preloads jQuery elements
+         *
+         * @name   Editor.init
+         * @function
+         */
+        me.init = function()
+        {
+            $editorTabs = $('#editorTabs');
+            $message = $('#message');
+
+            // bind events
+            $message.keydown(supportTabs);
+
+            // bind click events to tab switchers (a), but save parent of them
+            // (li)
+            $messageEdit = $('#messageedit').click(viewEditor).parent();
+            $messagePreview = $('#messagepreview').click(viewPreview).parent();
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * (view) Parse and show paste.
+     *
+     * @name   PasteViewer
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var PasteViewer = (function (window, document) {
+        var me = {};
+
+        var $placeholder,
+            $prettyMessage,
+            $prettyPrint,
+            $plainText;
+
+        var text,
+            format = 'plaintext',
+            isDisplayed = false,
+            isChanged = true; // by default true as nothing was parsed yet
+
+        /**
+         * apply the set format on paste and displays it
+         *
+         * @name   PasteViewer.parsePaste
+         * @private
+         * @function
+         */
+        function parsePaste()
+        {
+            // skip parsing if no text is given
+            if (text === '') {
+                return;
+            }
+
+            // set text
+            Helper.setElementText($plainText, text);
+            Helper.setElementText($prettyPrint, text);
+
+            switch (format) {
+                case 'markdown':
+                    var converter = new showdown.Converter({
+                        strikethrough: true,
+                        tables: true,
+                        tablesHeaderId: true
+                    });
+                    $plainText.html(
+                        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?
+                    if (typeof prettyPrint === 'function')
+                    {
+                        prettyPrint();
+                    }
+
+                    $prettyPrint.html(
+                        prettyPrintOne(
+                            Helper.htmlEntities(text), 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');
+            }
+        }
+
+        /**
+         * displays the paste
+         *
+         * @name   PasteViewer.showPaste
+         * @private
+         * @function
+         */
+        function showPaste()
+        {
+            // instead of "nothing" better display a placeholder
+            if (text === '') {
+                $placeholder.removeClass('hidden')
+                return;
+            }
+            // otherwise hide the placeholder
+            $placeholder.addClass('hidden')
+
+            switch (format) {
+                case 'markdown':
+                    $plainText.removeClass('hidden');
+                    $prettyMessage.addClass('hidden');
+                    break;
+                default:
+                    $plainText.addClass('hidden');
+                    $prettyMessage.removeClass('hidden');
+                    break;
+            }
+        }
+
+        /**
+         * sets the format in which the text is shown
+         *
+         * @name   PasteViewer.setFormat
+         * @function
+         * @param {string} newFormat the the new format
+         */
+        me.setFormat = function(newFormat)
+        {
+            // skip if there is no update
+            if (format === newFormat) {
+                return;
+            }
+
+            // needs to update display too, if from or to Markdown is switched
+            if (format === 'markdown' || newFormat === 'markdown') {
+                isDisplayed = false;
+            }
+
+            format = newFormat;
+            isChanged = true;
+        }
+
+        /**
+         * returns the current format
+         *
+         * @name   PasteViewer.getFormat
+         * @function
+         * @return {string}
+         */
+        me.getFormat = function()
+        {
+            return format;
+        }
+
+        /**
+         * returns whether the current view is pretty printed
+         *
+         * @name   PasteViewer.isPrettyPrinted
+         * @function
+         * @return {bool}
+         */
+        me.isPrettyPrinted = function()
+        {
+            return $prettyPrint.hasClass('prettyprinted');
+        }
+
+        /**
+         * sets the text to show
+         *
+         * @name   PasteViewer.setText
+         * @function
+         * @param {string} newText the text to show
+         */
+        me.setText = function(newText)
+        {
+            if (text !== newText) {
+                text = newText;
+                isChanged = true;
+            }
+        }
+
+        /**
+         * gets the current cached text
+         *
+         * @name   PasteViewer.getText
+         * @function
+         * @return {string}
+         */
+        me.getText = function(newText)
+        {
+            return text;
+        }
+
+        /**
+         * show/update the parsed text (preview)
+         *
+         * @name   PasteViewer.run
+         * @function
+         */
+        me.run = function()
+        {
+            if (isChanged) {
+                parsePaste();
+                isChanged = false;
+            }
+
+            if (!isDisplayed) {
+                showPaste();
+                isDisplayed = true;
+            }
+        }
+
+        /**
+         * hide parsed text (preview)
+         *
+         * @name   PasteViewer.hide
+         * @function
+         */
+        me.hide = function()
+        {
+            if (!isDisplayed) {
+                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
+            }
+
+            $plainText.addClass('hidden');
+            $prettyMessage.addClass('hidden');
+            $placeholder.addClass('hidden');
+
+            isDisplayed = false;
+        }
+
+        /**
+         * init status manager
+         *
+         * preloads jQuery elements
+         *
+         * @name   PasteViewer.init
+         * @function
+         */
+        me.init = function()
+        {
+            $placeholder = $('#placeholder');
+            $plainText = $('#plaintext');
+            $prettyMessage = $('#prettymessage');
+            $prettyPrint = $('#prettyprint');
+
+            // check requirements
+            if (typeof prettyPrintOne !== 'function') {
+                Alert.showError([
+                    'The library %s is not available. This may cause display errors.',
+                    'pretty print'
+                ]);
+            }
+            if (typeof showdown !== 'object') {
+                Alert.showError([
+                    'The library %s is not available. This may cause display errors.',
+                    'showdown'
+                ]);
+            }
+
+            // get default option from template/HTML or fall back to set value
+            format = Model.getFormatDefault() || format;
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * (view) Show attachment and preview if possible
+     *
+     * @name   AttachmentViewer
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var AttachmentViewer = (function (window, document) {
+        var me = {};
+
+        var $attachmentLink,
+            $attachmentPreview,
+            $attachment;
+
+        var attachmentChanged = false,
+            attachmentHasPreview = false;
+
+        /**
+         * sets the attachment but does not yet show it
+         *
+         * @name   AttachmentViewer.setAttachment
+         * @function
+         * @param {string} attachmentData - base64-encoded data of file
+         * @param {string} fileName - optional, file name
+         */
+        me.setAttachment = function(attachmentData, fileName)
+        {
+            var imagePrefix = 'data:image/';
+
+            $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;
+            }
+
+            attachmentChanged = true;
+        }
+
+        /**
+         * displays the attachment
+         *
+         * @name AttachmentViewer.showAttachment
+         * @function
+         */
+        me.showAttachment = function()
+        {
+            $attachment.removeClass('hidden');
+
+            if (attachmentHasPreview) {
+                $attachmentPreview.removeClass('hidden');
+            }
+        }
+
+        /**
+         * removes the attachment
+         *
+         * This automatically hides the attachment containers to, to
+         * prevent an inconsistent display.
+         *
+         * @name AttachmentViewer.removeAttachment
+         * @function
+         */
+        me.removeAttachment = function()
+        {
+            me.hideAttachment();
+            me.hideAttachmentPreview();
+            $attachmentLink.prop('href', '');
+            $attachmentLink.prop('download', '');
+            $attachmentPreview.html('');
+        }
+
+        /**
+         * hides the attachment
+         *
+         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
+         * for that) nor will it hide the attachment link if it was moved somewhere
+         * else (see AttachmentViewer.moveAttachmentTo).
+         *
+         * @name AttachmentViewer.hideAttachment
+         * @function
+         */
+        me.hideAttachment = function()
+        {
+            $attachment.addClass('hidden');
+        }
+
+        /**
+         * hides the attachment preview
+         *
+         * @name AttachmentViewer.hideAttachmentPreview
+         * @function
+         */
+        me.hideAttachmentPreview = function()
+        {
+            $attachmentPreview.addClass('hidden');
+        }
+
+        /**
+         * checks if there is an attachment
+         *
+         * @name   AttachmentViewer.hasAttachment
+         * @function
+         */
+        me.hasAttachment = function()
+        {
+            return ($attachmentLink.prop('href') !== '')
+        }
+
+        /**
+         * return the attachment
+         *
+         * @name   AttachmentViewer.getAttachment
+         * @function
+         * @returns {array}
+         */
+        me.getAttachment = function()
+        {
+            return [
+                $attachmentLink.prop('href'),
+                $attachmentLink.prop('download')
+            ];
+        }
+
+        /**
+         * moves the attachment link to another element
+         *
+         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
+         *
+         * @name   AttachmentViewer.moveAttachmentTo
+         * @function
+         * @param {jQuery} $element - the wrapper/container element where this should be moved to
+         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
+         */
+        me.moveAttachmentTo = function($element, label)
+        {
+            // move elemement to new place
+            $attachmentLink.appendTo($element);
+
+            // update text
+            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
+        }
+
+        /**
+         * initiate
+         *
+         * preloads jQuery elements
+         *
+         * @name   AttachmentViewer.init
+         * @function
+         */
+        me.init = function()
+        {
+            $attachment = $('#attachment');
+            $attachmentLink = $('#attachment a');
+            $attachmentPreview = $('#attachmentPreview');
+        }
+
+        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 me = {};
+
+        var $commentTail,
+            $discussion,
+            $reply,
+            $replyMessage,
+            $replyNickname,
+            $replyStatus,
+            $commentContainer;
+
+        var replyCommentId;
+
+        /**
+         * initializes the templates
+         *
+         * @name   DiscussionViewer.initTemplates
+         * @private
+         * @function
+         */
+        function initTemplates()
+        {
+            $reply = Model.getTemplate('reply');
+            $replyMessage = $reply.find('#replymessage');
+            $replyNickname = $reply.find('#nickname');
+            $replyStatus = $reply.find('#replystatus');
+
+            // cache jQuery elements
+            $commentTail = Model.getTemplate('commenttail');
+        }
+
+        /**
+         * open the comment entry when clicking the "Reply" button of a comment
+         *
+         * @name   DiscussionViewer.openReply
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function openReply(event)
+        {
+            var $source = $(event.target);
+
+            // clear input
+            $replyMessage.val('');
+            $replyNickname.val('');
+
+            // get comment id from source element
+            replyCommentId = $source.parent().prop('id').split('_')[1];
+
+            // move to correct position
+            $source.after($reply);
+
+            // show
+            $reply.removeClass('hidden');
+            $replyMessage.focus();
+
+            event.preventDefault();
+        }
+
+        /**
+         * custom handler for displaying notifications in own status message area
+         *
+         * @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)
+        {
+            // ignore loading messages
+            if (alertType === 'loading') {
+                return false;
+            }
+
+            if (alertType === 'danger') {
+                $replyStatus.removeClass('alert-info');
+                $replyStatus.addClass('alert-danger');
+                $replyStatus.find(':first').removeClass('glyphicon-alert');
+                $replyStatus.find(':first').addClass('glyphicon-info-sign');
+            } else {
+                $replyStatus.removeClass('alert-danger');
+                $replyStatus.addClass('alert-info');
+                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
+                $replyStatus.find(':first').addClass('glyphicon-alert');
+            }
+
+            return $replyStatus;
+        }
+
+        /**
+         * adds another comment
+         *
+         * @name   DiscussionViewer.addComment
+         * @function
+         * @param {object} comment
+         * @param {string} commentText
+         * @param {jQuery} $place      - optional, tries to find the best position otherwise
+         */
+        me.addComment = function(comment, commentText, nickname, $place)
+        {
+            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';
+            }
+
+            // create new comment based on template
+            var $commentEntry = Model.getTemplate('comment');
+            $commentEntry.prop('id', 'comment_' + comment.id);
+            var $commentEntryData = $commentEntry.find('div.commentdata');
+
+            // set & parse text
+            Helper.setElementText($commentEntryData, commentText);
+            Helper.urls2links($commentEntryData);
+
+            // set nickname
+            if (nickname.length > 0) {
+                $commentEntry.find('span.nickname').text(nickname);
+            } else {
+                $commentEntry.find('span.nickname').html('<i></i>');
+                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
+            }
+
+            // set date
+            $commentEntry.find('span.commentdate')
+                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
+                      .attr('title', 'CommentID: ' + comment.id);
+
+            // if an avatar is available, display it
+            if (comment.meta.vizhash) {
+                $commentEntry.find('span.nickname')
+                             .before(
+                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
+                             );
+                $(document).on('languageLoaded', function () {
+                    $commentEntry.find('img.vizhash')
+                                 .prop('title', I18n._('Avatar generated from IP address'));
+                });
+            }
+
+            // finally append comment
+            $place.append($commentEntry);
+        }
+
+        /**
+         * finishes the discussion area after last comment
+         *
+         * @name   DiscussionViewer.finishDiscussion
+         * @function
+         */
+        me.finishDiscussion = function()
+        {
+            // add 'add new comment' area
+            $commentContainer.append($commentTail);
+
+            // 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
+         * @function
+         */
+        me.prepareNewDisucssion = function()
+        {
+            $commentContainer.html('');
+            $discussion.addClass('hidden');
+
+            // (re-)init templates
+            initTemplates();
+        }
+
+        /**
+         * returns the user put into the reply form
+         *
+         * @name   DiscussionViewer.getReplyData
+         * @function
+         * @return {array}
+         */
+        me.getReplyData = function()
+        {
+            return [
+                $replyMessage.val(),
+                $replyNickname.val()
+            ];
+        }
+
+        /**
+         * highlights a specific comment and scrolls to it if necessary
+         *
+         * @name   DiscussionViewer.highlightComment
+         * @function
+         * @param {string} commentId
+         * @param {bool} fadeOut - whether to fade out the comment
+         */
+        me.highlightComment = function(commentId, fadeOut)
+        {
+            var $comment = $('#comment_' + commentId);
+            // in case comment does not exist, cancel
+            if ($comment.length === 0) {
+                return;
+            }
+
+            var highlightComment = function () {
+                $comment.addClass('highlight');
+                if (fadeOut === true) {
+                    setTimeout(function () {
+                        $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
+         *
+         * preloads jQuery elements
+         *
+         * @name   DiscussionViewer.init
+         * @function
+         */
+        me.init = function()
+        {
+            // bind events to templates (so they are later cloned)
+            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
+            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
+
+            $commentContainer = $('#commentcontainer');
+            $discussion = $('#discussion');
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * Manage top (navigation) bar
+     *
+     * @name   TopNav
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var TopNav = (function (window, document) {
+        var me = {};
+
+        var createButtonsDisplayed = false;
+        var viewButtonsDisplayed = false;
+
+        var $attach,
+            $burnAfterReading,
+            $burnAfterReadingOption,
+            $cloneButton,
+            $customAttachment,
+            $expiration,
+            $fileRemoveButton,
+            $fileWrap,
+            $formatter,
+            $newButton,
+            $openDiscussion,
+            $openDiscussionOption,
+            $password,
+            $passwordInput,
+            $rawTextButton,
+            $sendButton;
+
+        var pasteExpiration = '1week';
+
+        /**
+         * set the expiration on bootstrap templates in dropdown
+         *
+         * @name   TopNav.updateExpiration
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function updateExpiration(event)
+        {
+            // get selected option
+            var target = $(event.target);
+
+            // update dropdown display and save new expiration time
+            $('#pasteExpirationDisplay').text(target.text());
+            pasteExpiration = target.data('expiration');
+
+            event.preventDefault();
+        }
+
+        /**
+         * set the format on bootstrap templates in dropdown
+         *
+         * @name   TopNav.updateFormat
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function updateFormat(event)
+        {
+            // get selected option
+            var $target = $(event.target);
+
+            // update dropdown display and save new format
+            var newFormat = $target.data('format');
+            $('#pasteFormatterDisplay').text($target.text());
+            PasteViewer.setFormat(newFormat);
+
+            // update preview
+            if (Editor.isPreview()) {
+                PasteViewer.run();
+            }
+
+            event.preventDefault();
+        }
+
+        /**
+         * when "burn after reading" is checked, disable discussion
+         *
+         * @name   TopNav.changeBurnAfterReading
+         * @private
+         * @function
+         */
+        function changeBurnAfterReading()
+        {
+            if ($burnAfterReading.is(':checked')) {
+                $openDiscussionOption.addClass('buttondisabled');
+                $openDiscussion.prop('checked', false);
+
+                // if button is actually disabled, force-enable it and uncheck other button
+                $burnAfterReadingOption.removeClass('buttondisabled');
+            } else {
+                $openDiscussionOption.removeClass('buttondisabled');
+            }
+        }
+
+        /**
+         * when discussion is checked, disable "burn after reading"
+         *
+         * @name   TopNav.changeOpenDiscussion
+         * @private
+         * @function
+         */
+        function changeOpenDiscussion()
+        {
+            if ($openDiscussion.is(':checked')) {
+                $burnAfterReadingOption.addClass('buttondisabled');
+                $burnAfterReading.prop('checked', false);
+
+                // if button is actually disabled, force-enable it and uncheck other button
+                $openDiscussionOption.removeClass('buttondisabled');
+            } else {
+                $burnAfterReadingOption.removeClass('buttondisabled');
+            }
+        }
+
+        /**
+         * return raw text
+         *
+         * @name   TopNav.rawText
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function rawText(event)
+        {
+            TopNav.hideAllButtons();
+            Alert.showLoading('Showing raw text…', 0, 'time');
+            var paste = PasteViewer.getText();
+
+            // push a new state to allow back navigation with browser back button
+            history.pushState(
+                {type: 'raw'},
+                document.title,
+                // recreate paste URL
+                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
+                Model.getPasteKey()
+            );
+
+            // we use text/html instead of text/plain to avoid a bug when
+            // reloading the raw text view (it reverts to type text/html)
+            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
+            var newDoc = document.open('text/html', 'replace');
+            newDoc.write('<!DOCTYPE html><html><head>');
+            for (var i = 0; i < $head.length; i++) {
+                newDoc.write($head[i].outerHTML);
+            }
+            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
+            newDoc.close();
+        }
+
+        /**
+         * saves the language in a cookie and reloads the page
+         *
+         * @name   TopNav.setLanguage
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function setLanguage(event)
+        {
+            document.cookie = 'lang=' + $(event.target).data('lang');
+            UiHelper.reloadHome();
+        }
+
+        /**
+         * hides all messages and creates a new paste
+         *
+         * @name   TopNav.clickNewPaste
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function clickNewPaste(event)
+        {
+            Controller.hideStatusMessages();
+            Controller.newPaste();
+        }
+
+        /**
+         * removes the existing attachment
+         *
+         * @name   TopNav.removeAttachment
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function removeAttachment(event)
+        {
+            // if custom attachment is used, remove it first
+            if (!$customAttachment.hasClass('hidden')) {
+                AttachmentViewer.removeAttachment();
+                $customAttachment.addClass('hidden');
+                $fileWrap.removeClass('hidden');
+            }
+
+            // our up-to-date jQuery can handle it :)
+            $fileWrap.find('input').val('');
+
+            // pevent '#' from appearing in the URL
+            event.preventDefault();
+        }
+
+        /**
+         * Shows all elements belonging to viwing an existing pastes
+         *
+         * @name   TopNav.showViewButtons
+         * @function
+         */
+        me.showViewButtons = function()
+        {
+            if (viewButtonsDisplayed) {
+                console.log('showViewButtons: view buttons are already displayed');
+                return;
+            }
+
+            $newButton.removeClass('hidden');
+            $cloneButton.removeClass('hidden');
+            $rawTextButton.removeClass('hidden');
+
+            viewButtonsDisplayed = true;
+        }
+
+        /**
+         * Hides all elements belonging to existing pastes
+         *
+         * @name   TopNav.hideViewButtons
+         * @function
+         */
+        me.hideViewButtons = function()
+        {
+            if (!viewButtonsDisplayed) {
+                console.log('hideViewButtons: view buttons are already hidden');
                 return;
             }
 
-            // Keep line height even if content empty.
-            this.status.html(' ');
+            $newButton.addClass('hidden');
+            $cloneButton.addClass('hidden');
+            $rawTextButton.addClass('hidden');
+
+            viewButtonsDisplayed = false;
+        }
+
+        /**
+         * Hides all elements belonging to existing pastes
+         *
+         * @name   TopNav.hideAllButtons
+         * @function
+         */
+        me.hideAllButtons = function()
+        {
+            me.hideViewButtons();
+            me.hideCreateButtons();
+        }
+
+        /**
+         * shows all elements needed when creating a new paste
+         *
+         * @name   TopNav.showCreateButtons
+         * @function
+         */
+        me.showCreateButtons = function()
+        {
+            if (createButtonsDisplayed) {
+                console.log('showCreateButtons: create buttons are already displayed');
+                return;
+            }
+
+            $sendButton.removeClass('hidden');
+            $expiration.removeClass('hidden');
+            $formatter.removeClass('hidden');
+            $burnAfterReadingOption.removeClass('hidden');
+            $openDiscussionOption.removeClass('hidden');
+            $newButton.removeClass('hidden');
+            $password.removeClass('hidden');
+            $attach.removeClass('hidden');
+
+            createButtonsDisplayed = true;
+        }
+
+        /**
+         * shows all elements needed when creating a new paste
+         *
+         * @name   TopNav.hideCreateButtons
+         * @function
+         */
+        me.hideCreateButtons = function()
+        {
+            if (!createButtonsDisplayed) {
+                console.log('hideCreateButtons: create buttons are already hidden');
+                return;
+            }
+
+            $newButton.addClass('hidden');
+            $sendButton.addClass('hidden');
+            $expiration.addClass('hidden');
+            $formatter.addClass('hidden');
+            $burnAfterReadingOption.addClass('hidden');
+            $openDiscussionOption.addClass('hidden');
+            $password.addClass('hidden');
+            $attach.addClass('hidden');
+
+            createButtonsDisplayed = false;
+        }
+
+        /**
+         * only shows the "new paste" button
+         *
+         * @name   TopNav.showNewPasteButton
+         * @function
+         */
+        me.showNewPasteButton = function()
+        {
+            $newButton.removeClass('hidden');
+        }
+
+        /**
+         * only hides the clone button
+         *
+         * @name   TopNav.hideCloneButton
+         * @function
+         */
+        me.hideCloneButton = function()
+        {
+            $cloneButton.addClass('hidden');
+        }
+
+        /**
+         * only hides the raw text button
+         *
+         * @name   TopNav.hideRawButton
+         * @function
+         */
+        me.hideRawButton = function()
+        {
+            $rawTextButton.addClass('hidden');
+        }
+
+        /**
+         * hides the file selector in attachment
+         *
+         * @name   TopNav.hideFileSelector
+         * @function
+         */
+        me.hideFileSelector = function()
+        {
+            $fileWrap.addClass('hidden');
+        }
+
+
+        /**
+         * shows the custom attachment
+         *
+         * @name   TopNav.showCustomAttachment
+         * @function
+         */
+        me.showCustomAttachment = function()
+        {
+            $customAttachment.removeClass('hidden');
+        }
+
+        /**
+         * collapses the navigation bar if nedded
+         *
+         * @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();
+            }
+        }
+
+        /**
+         * returns the currently set expiration time
+         *
+         * @name   TopNav.getExpiration
+         * @function
+         * @return {int}
+         */
+        me.getExpiration = function()
+        {
+            return pasteExpiration;
+        }
+
+        /**
+         * returns the currently selected file(s)
+         *
+         * @name   TopNav.getFileList
+         * @function
+         * @return {FileList|null}
+         */
+        me.getFileList = function()
+        {
+            var $file = $('#file');
+
+            // if no file given, return null
+            if (!$file.length || !$file[0].files.length) {
+                return null;
+            }
+            // @TODO is this really necessary
+            if (!($file[0].files && $file[0].files[0])) {
+                return null;
+            }
+
+            return $file[0].files;
+        }
+
+        /**
+         * returns the state of the burn after reading checkbox
+         *
+         * @name   TopNav.getExpiration
+         * @function
+         * @return {bool}
+         */
+        me.getBurnAfterReading = function()
+        {
+            return $burnAfterReading.is(':checked');
+        }
+
+        /**
+         * returns the state of the discussion checkbox
+         *
+         * @name   TopNav.getOpenDiscussion
+         * @function
+         * @return {bool}
+         */
+        me.getOpenDiscussion = function()
+        {
+            return $openDiscussion.is(':checked');
+        }
+
+        /**
+         * returns the entered password
+         *
+         * @name   TopNav.getPassword
+         * @function
+         * @return {string}
+         */
+        me.getPassword = function()
+        {
+            return $passwordInput.val();
+        }
+
+        /**
+         * returns the element where custom attachments can be placed
+         *
+         * Used by AttachmentViewer when an attachment is cloned here.
+         *
+         * @name   TopNav.getCustomAttachment
+         * @function
+         * @return {jQuery}
+         */
+        me.getCustomAttachment = function()
+        {
+            return $customAttachment;
+        }
+
+        /**
+         * init navigation manager
+         *
+         * preloads jQuery elements
+         *
+         * @name   TopNav.init
+         * @function
+         */
+        me.init = function()
+        {
+            $attach = $('#attach');
+            $burnAfterReading = $('#burnafterreading');
+            $burnAfterReadingOption = $('#burnafterreadingoption');
+            $cloneButton = $('#clonebutton');
+            $customAttachment = $('#customattachment');
+            $expiration = $('#expiration');
+            $fileRemoveButton = $('#fileremovebutton');
+            $fileWrap = $('#filewrap');
+            $formatter = $('#formatter');
+            $newButton = $('#newbutton');
+            $openDiscussion = $('#opendiscussion');
+            $openDiscussionOption = $('#opendiscussionoption');
+            $password = $('#password');
+            $passwordInput = $('#passwordinput');
+            $rawTextButton = $('#rawtextbutton');
+            $sendButton = $('#sendbutton');
+
+            // bootstrap template drop down
+            $('#language ul.dropdown-menu li a').click(setLanguage);
+            // page template drop down
+            $('#language select option').click(setLanguage);
+
+            // bind events
+            $burnAfterReading.change(changeBurnAfterReading);
+            $openDiscussionOption.change(changeOpenDiscussion);
+            $newButton.click(clickNewPaste);
+            $sendButton.click(PasteEncrypter.sendPaste);
+            $cloneButton.click(Controller.clonePaste);
+            $rawTextButton.click(rawText);
+            $fileRemoveButton.click(removeAttachment);
+
+            // bootstrap template drop downs
+            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
+            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
+
+            // initiate default state of checkboxes
+            changeBurnAfterReading();
+            changeOpenDiscussion();
+
+            // get default value from template or fall back to set value
+            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
+        }
+
+        return me;
+    })(window, document);
+
+    /**
+     * Responsible for AJAX requests, transparently handles encryption…
+     *
+     * @name   Uploader
+     * @class
+     */
+    var Uploader = (function () {
+        var me = {};
+
+        var successFunc = null,
+            failureFunc = null,
+            url,
+            data,
+            symmetricKey,
+            password;
+
+        /**
+         * public variable ('constant') for errors to prevent magic numbers
+         *
+         * @name   Uploader.error
+         * @readonly
+         * @enum   {Object}
+         */
+        me.error = {
+            okay: 0,
+            custom: 1,
+            unknown: 2,
+            serverError: 3
+        };
+
+        /**
+         * ajaxHeaders to send in AJAX requests
+         *
+         * @name   Uploader.ajaxHeaders
+         * @private
+         * @readonly
+         * @enum   {Object}
+         */
+        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
+
+        /**
+         * called after successful upload
+         *
+         * @name   Uploader.checkCryptParameters
+         * @private
+         * @function
+         * @throws {string}
+         */
+        function checkCryptParameters()
+        {
+            // workaround for this nasty 'bug' in ECMAScript
+            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
+            var typeOfKey = typeof symmetricKey;
+            if (symmetricKey === null) {
+                typeOfKey = 'null';
+            }
+
+            // in case of missing preparation, throw error
+            switch (typeOfKey) {
+                case 'string':
+                    // already set, all right
+                    return;
+                case 'null':
+                    // needs to be generated auto-generate
+                    symmetricKey = CryptTool.getSymmetricKey();
+                    break;
+                default:
+                    console.error('current invalid symmetricKey:', symmetricKey);
+                    throw 'symmetricKey is invalid, probably the module was not prepared';
+            }
+            // password is optional
+        }
+
+        /**
+         * called after successful upload
+         *
+         * @name   Uploader.success
+         * @private
+         * @function
+         * @param {int} status
+         * @param {int} data - optional
+         */
+        function success(status, result)
+        {
+            // add useful data to result
+            result.encryptionKey = symmetricKey;
+            result.requestData = data;
+
+            if (successFunc !== null) {
+                successFunc(status, result);
+            }
+        }
+
+        /**
+         * called after a upload failure
+         *
+         * @name   Uploader.fail
+         * @private
+         * @function
+         * @param {int} status - internal code
+         * @param {int} data - original error code
+         */
+        function fail(status, result)
+        {
+            if (failureFunc !== null) {
+                failureFunc(status, result);
+            }
+        }
+
+        /**
+         * actually uploads the data
+         *
+         * @name   Uploader.run
+         * @function
+         */
+        me.run = function()
+        {
+            $.ajax({
+                type: 'POST',
+                url: url,
+                data: data,
+                dataType: 'json',
+                headers: ajaxHeaders,
+                success: function(result) {
+                    if (result.status === 0) {
+                        success(0, result);
+                    } else if (result.status === 1) {
+                        fail(1, result);
+                    } else {
+                        fail(2, result);
+                    }
+                }
+            })
+            .fail(function(jqXHR, textStatus, errorThrown) {
+                console.error(textStatus, errorThrown);
+                fail(3, jqXHR);
+            });
+        }
+
+        /**
+         * set success function
+         *
+         * @name   Uploader.setUrl
+         * @function
+         * @param {function} func
+         */
+        me.setUrl = function(newUrl)
+        {
+            url = newUrl;
+        }
+
+        /**
+         * sets the password to use (first value) and optionally also the
+         * encryption key (not recommend, it is automatically generated).
+         *
+         * Note: Call this after prepare() as prepare() resets these values.
+         *
+         * @name   Uploader.setCryptValues
+         * @function
+         * @param {string} newPassword
+         * @param {string} newKey       - optional
+         */
+        me.setCryptParameters = function(newPassword, newKey)
+        {
+            password = newPassword;
+
+            if (typeof newKey !== 'undefined') {
+                symmetricKey = newKey;
+            }
+        }
+
+        /**
+         * set success function
+         *
+         * @name   Uploader.setSuccess
+         * @function
+         * @param {function} func
+         */
+        me.setSuccess = function(func)
+        {
+            successFunc = func;
+        }
+
+        /**
+         * set failure function
+         *
+         * @name   Uploader.setFailure
+         * @function
+         * @param {function} func
+         */
+        me.setFailure = function(func)
+        {
+            failureFunc = func;
+        }
+
+        /**
+         * prepares a new upload
+         *
+         * Call this when doing a new upload to reset any data from potential
+         * previous uploads. Must be called before any other method of this
+         * module.
+         *
+         * @name   Uploader.prepare
+         * @function
+         * @return {object}
+         */
+        me.prepare = function()
+        {
+            // entropy should already be checked!
+
+            // reset password
+            password = '';
+
+            // reset key, so it a new one is generated when it is used
+            symmetricKey = null;
+
+            // reset data
+            successFunc = null;
+            failureFunc = null;
+            url = Helper.baseUri();
+            data = {};
+        }
+
+        /**
+         * encrypts and sets the data
+         *
+         * @name   Uploader.setData
+         * @function
+         * @param {string} index
+         * @param {mixed} element
+         */
+        me.setData = function(index, element)
+        {
+            checkCryptParameters();
+            data[index] = CryptTool.cipher(symmetricKey, password, element);
+        }
+
+        /**
+         * set the additional metadata to send unencrypted
+         *
+         * @name   Uploader.setUnencryptedData
+         * @function
+         * @param {string} index
+         * @param {mixed} element
+         */
+        me.setUnencryptedData = function(index, element)
+        {
+            data[index] = element;
+        }
+
+        /**
+         * set the additional metadata to send unencrypted passed at once
+         *
+         * @name   Uploader.setUnencryptedData
+         * @function
+         * @param {object} newData
+         */
+        me.setUnencryptedBulkData = function(newData)
+        {
+            $.extend(data, newData);
+        }
+
+        /**
+         * Helper, which parses shows a general error message based on the result of the Uploader
+         *
+         * @name    Uploader.parseUploadError
+         * @function
+         * @param {int} status
+         * @param {object} data
+         * @param {string} doThisThing - a human description of the action, which was tried
+         * @return {array}
+         */
+        me.parseUploadError = function(status, data, doThisThing) {
+            var errorArray = ['Error while parsing error message.'];
+
+            switch (status) {
+                case Uploader.error['custom']:
+                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
+                    break;
+                case Uploader.error['unknown']:
+                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
+                    break;
+                case Uploader.error['serverError']:
+                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];                        break;
+                default:
+                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
+                    break;
+            }
+
+            return errorArray;
+        }
 
-            // Display an existing paste
-            if (this.cipherData.text().length > 1)
-            {
-                // Missing decryption key in URL?
-                if (window.location.hash.length === 0)
-                {
-                    this.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)'));
+        /**
+         * init Uploader
+         *
+         * @name   Uploader.init
+         * @function
+         */
+        me.init = function()
+        {
+            // nothing yet
+        }
+
+        return me;
+    })();
+
+    /**
+     * (controller) Responsible for encrypting paste and sending it to server.
+     *
+     * Does upload, encryption is done transparently by Uploader.
+     *
+     * @name PasteEncrypter
+     * @class
+     */
+    var PasteEncrypter = (function () {
+        var me = {};
+
+        var requirementsChecked = false;
+
+        /**
+         * checks whether there is a suitable amount of entrophy
+         *
+         * @name PasteEncrypter.checkRequirements
+         * @private
+         * @function
+         * @param {function} retryCallback - the callback to execute to retry the upload
+         * @return {bool}
+         */
+        function checkRequirements(retryCallback) {
+            // skip double requirement checks
+            if (requirementsChecked === true) {
+                return true;
+            }
+
+            if (!CryptTool.isEntropyReady()) {
+                // display a message and wait
+                Alert.showStatus('Please move your mouse for more entropy…');
+
+                CryptTool.addEntropySeedListener(retryCallback);
+                return false;
+            }
+
+            requirementsChecked = true;
+
+            return true;
+        }
+
+        /**
+         * called after successful paste upload
+         *
+         * @name PasteEncrypter.showCreatedPaste
+         * @private
+         * @function
+         * @param {int} status
+         * @param {object} data
+         */
+        function showCreatedPaste(status, data) {
+            Alert.hideLoading();
+
+            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
+                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
+
+            Alert.hideMessages();
+
+            // show notification
+            PasteStatus.createPasteNotification(url, deleteUrl)
+
+            // show new URL in browser bar
+            history.pushState({type: 'newpaste'}, document.title, url);
+
+            TopNav.showViewButtons();
+            TopNav.hideRawButton();
+            Editor.hide();
+
+            // parse and show text
+            // (preparation already done in me.sendPaste())
+            PasteViewer.run();
+        }
+
+        /**
+         * called after successful comment upload
+         *
+         * @name PasteEncrypter.showUploadedComment
+         * @private
+         * @function
+         * @param {int} status
+         * @param {object} data
+         */
+        function showUploadedComment(status, data) {
+            // show success message
+            // Alert.showStatus('Comment posted.');
+
+            // reload paste
+            Controller.refreshPaste(function () {
+                // highlight sent comment
+                DiscussionViewer.highlightComment(data.id, true);
+                // reset error handler
+                Alert.setCustomHandler(null);
+            });
+        }
+
+        /**
+         * adds attachments to the Uploader
+         *
+         * @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) {
+            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;
                 }
 
-                // List of messages to display.
-                var data = $.parseJSON(this.cipherData.text());
+                var reader = new FileReader();
+
+                // 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);
+            } else if (AttachmentViewer.hasAttachment()) {
+                // fall back to cloned part
+                var attachment = AttachmentViewer.getAttachment();
 
-                // Show proper elements on screen.
-                this.stateExistingPaste();
+                Uploader.setData('attachment', attachment[0]);
+                Uploader.setData('attachmentname', attachment[1]);
+                return callback();
+            } else {
+                // if there are no attachments, this is of course still successful
+                return callback();
+            }
+        }
 
-                this.displayMessages(this.pageKey(), data);
+        /**
+         * send a reply in a discussion
+         *
+         * @name   PasteEncrypter.sendComment
+         * @function
+         */
+        me.sendComment = function()
+        {
+            Alert.hideMessages();
+            Alert.setCustomHandler(DiscussionViewer.handleNotification);
+
+            // UI loading state
+            TopNav.hideAllButtons();
+            Alert.showLoading('Sending comment…', 0, '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],
+                parentid = DiscussionViewer.getReplyCommentId();
+
+            // do not send if there is no data
+            if (plainText.length === 0) {
+                // revert loading status…
+                Alert.hideLoading();
+                Alert.setCustomHandler(null);
+                TopNav.showViewButtons();
+                return;
             }
-            // Display error message from php code.
-            else if (this.errorMessage.text().length > 1)
-            {
-                this.showError(this.errorMessage.text());
+
+            // check entropy
+            if (!checkRequirements(function () {
+                me.sendComment();
+            })) {
+                return; // to prevent multiple executions
             }
-            // Create a new paste.
-            else
-            {
-                this.newPaste();
+            Alert.showLoading(null, 10);
+
+            // prepare Uploader
+            Uploader.prepare();
+            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
+
+            // set success/fail functions
+            Uploader.setSuccess(showUploadedComment);
+            Uploader.setFailure(function (status, data) {
+                // revert loading status…
+                Alert.hideLoading();
+                TopNav.showViewButtons();
+
+                // show error message
+                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
+
+                // reset error handler
+                Alert.setCustomHandler(null);
+            });
+
+            // fill it with unencrypted params
+            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?
+                Uploader.setUnencryptedData('parentid', Model.getPasteId());
+            } else {
+                Uploader.setUnencryptedData('parentid', parentid);
+            }
+
+            // encrypt data
+            Uploader.setData('data', plainText);
+
+            if (nickname.length > 0) {
+                Uploader.setData('nickname', nickname);
+            }
+
+            Uploader.run();
+        }
+
+        /**
+         * sends a new paste to server
+         *
+         * @name   PasteEncrypter.sendPaste
+         * @function
+         */
+        me.sendPaste = function()
+        {
+            // hide previous (error) messages
+            Controller.hideStatusMessages();
+
+            // UI loading state
+            TopNav.hideAllButtons();
+            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
+            TopNav.collapseBar();
+
+            // get data
+            var plainText = Editor.getText(),
+                format = PasteViewer.getFormat(),
+                files = TopNav.getFileList();
+
+            // do not send if there is no data
+            if (plainText.length === 0 && files === null) {
+                // revert loading status…
+                Alert.hideLoading();
+                TopNav.showCreateButtons();
+                return;
+            }
+
+            Alert.showLoading(null, 10);
+
+            // check entropy
+            if (!checkRequirements(function () {
+                me.sendPaste();
+            })) {
+                return; // to prevent multiple executions
             }
+
+            // prepare Uploader
+            Uploader.prepare();
+            Uploader.setCryptParameters(TopNav.getPassword());
+
+            // set success/fail functions
+            Uploader.setSuccess(showCreatedPaste);
+            Uploader.setFailure(function (status, data) {
+                // revert loading status…
+                Alert.hideLoading();
+                TopNav.showCreateButtons();
+
+                // show error message
+                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
+            });
+
+            // fill it with unencrypted submitted options
+            Uploader.setUnencryptedBulkData({
+                expire:           TopNav.getExpiration(),
+                formatter:        format,
+                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
+                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
+            });
+
+            // prepare PasteViewer for later preview
+            PasteViewer.setText(plainText);
+            PasteViewer.setFormat(format);
+
+            // encrypt cipher data
+            Uploader.setData('data', plainText);
+
+            // encrypt attachments
+            encryptAttachments(
+                files === null ? null : files[0],
+                function () {
+                    // send data
+                    Uploader.run();
+                }
+            );
+        }
+
+        /**
+         * initialize
+         *
+         * @name   PasteEncrypter.init
+         * @function
+         */
+        me.init = function()
+        {
+            // nothing yet
         }
-    }
+
+        return me;
+    })();
 
     /**
-     * main application start, called when DOM is fully loaded
-     * runs privatebin when translations were loaded
+     * (controller) Responsible for decrypting cipherdata and passing data to view.
+     *
+     * Only decryption, no download.
+     *
+     * @name PasteDecrypter
+     * @class
      */
-    i18n.loadTranslations($.proxy(privatebin.init, privatebin));
-});
+    var PasteDecrypter = (function () {
+        var me = {};
+
+        /**
+         * decrypt data or prompts for password in cvase of failure
+         *
+         * @name   PasteDecrypter.decryptOrPromptPassword
+         * @private
+         * @function
+         * @param  {string} key
+         * @param  {string} password - optional, may be an empty string
+         * @param  {string} cipherdata
+         * @throws {string}
+         * @return {false|string} false, when unsuccessful or string (decrypted data)
+         */
+        function decryptOrPromptPassword(key, password, cipherdata)
+        {
+            // try decryption without password
+            var plaindata = CryptTool.decipher(key, password, cipherdata);
+
+            // if it fails, request password
+            if (plaindata.length === 0 && password.length === 0) {
+                // try to get cached password first
+                password = Prompt.getPassword();
+
+                // if password is there, re-try
+                if (password.length === 0) {
+                    password = Prompt.requestPassword();
+                }
+                // recursive
+                // note: an infinite loop is prevented as the previous if
+                // clause checks whether a password is already set and ignores
+                // errors when a password has been passed
+                return decryptOrPromptPassword.apply(key, password, cipherdata);
+            }
+
+            // if all tries failed, we can only return an error
+            if (plaindata.length === 0) {
+                throw 'failed to decipher data';
+            }
+
+            return plaindata;
+        }
+
+        /**
+         * decrypt the actual paste text
+         *
+         * @name   PasteDecrypter.decryptOrPromptPassword
+         * @private
+         * @function
+         * @param  {object} paste - paste data in object form
+         * @param  {string} key
+         * @param  {string} password
+         * @param  {bool} ignoreError - ignore decryption errors iof set to true
+         * @return {bool} whether action was successful
+         * @throws {string}
+         */
+        function decryptPaste(paste, key, password, ignoreError)
+        {
+            var plaintext
+            if (ignoreError === true) {
+                plaintext = CryptTool.decipher(key, password, paste.data);
+            } else {
+                try {
+                    plaintext = decryptOrPromptPassword(key, password, paste.data);
+                } catch (err) {
+                    throw 'failed to decipher paste text: ' + err
+                }
+                if (plaintext === false) {
+                    return false;
+                }
+            }
+
+            // on success show paste
+            PasteViewer.setFormat(paste.meta.formatter);
+            PasteViewer.setText(plaintext);
+            // trigger to show the text (attachment loaded afterwards)
+            PasteViewer.run();
+
+            return true;
+        }
+
+        /**
+         * decrypts any attachment
+         *
+         * @name   PasteDecrypter.decryptAttachment
+         * @private
+         * @function
+         * @param  {object} paste - paste data in object form
+         * @param  {string} key
+         * @param  {string} password
+         * @return {bool} whether action was successful
+         * @throws {string}
+         */
+        function decryptAttachment(paste, key, password)
+        {
+            // decrypt attachment
+            try {
+                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
+            } catch (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
+                }
+                if (attachmentName === false) {
+                    return false;
+                }
+            }
+
+            AttachmentViewer.setAttachment(attachment, attachmentName);
+            AttachmentViewer.showAttachment();
+
+            return true;
+        }
+
+        /**
+         * decrypts all comments and shows them
+         *
+         * @name   PasteDecrypter.decryptComments
+         * @private
+         * @function
+         * @param  {object} paste - paste data in object form
+         * @param  {string} key
+         * @param  {string} password
+         * @return {bool} whether action was successful
+         */
+        function decryptComments(paste, key, password)
+        {
+            // remove potentially previous discussion
+            DiscussionViewer.prepareNewDisucssion();
+
+            // iterate over comments
+            for (var i = 0; i < paste.comments.length; ++i) {
+                var comment = paste.comments[i];
+
+                DiscussionViewer.addComment(
+                    comment,
+                    CryptTool.decipher(key, password, comment.data),
+                    CryptTool.decipher(key, password, comment.meta.nickname)
+                );
+            }
+
+            DiscussionViewer.finishDiscussion();
+            DiscussionViewer.showDiscussion();
+            return true;
+        }
+
+        /**
+         * show decrypted text in the display area, including discussion (if open)
+         *
+         * @name   PasteDecrypter.run
+         * @function
+         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
+         */
+        me.run = function(paste)
+        {
+            Alert.hideMessages();
+            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
+
+            if (typeof paste === 'undefined') {
+                paste = $.parseJSON(Model.getCipherData());
+            }
+
+            var key = Model.getPasteKey(),
+                password = Prompt.getPassword();
+
+            if (PasteViewer.isPrettyPrinted()) {
+                console.error('Too pretty! (don\'t know why this check)'); //@TODO
+                return;
+            }
+
+            // try to decrypt the paste
+            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;
+                    }
+                    // ignore empty paste, as this is allowed when pasting attachments
+                    decryptPaste(paste, key, password, true);
+                } else {
+                    decryptPaste(paste, key, password);
+                }
+
+
+                // shows the remaining time (until) deletion
+                PasteStatus.showRemainingTime(paste.meta);
+
+                // if the discussion is opened on this paste, display it
+                if (paste.meta.opendiscussion) {
+                    decryptComments(paste, key, password);
+                }
+
+                Alert.hideLoading();
+                TopNav.showViewButtons();
+            } catch(err) {
+                Alert.hideLoading();
+
+                // log and show error
+                console.error(err);
+                Alert.showError('Could not decrypt data (Wrong key?)');
+            }
+        }
+
+        /**
+         * initialize
+         *
+         * @name   PasteDecrypter.init
+         * @function
+         */
+        me.init = function()
+        {
+            // nothing yet
+        }
+
+        return me;
+    })();
+
+    /**
+     * (controller) main PrivateBin logic
+     *
+     * @name   Controller
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var Controller = (function (window, document) {
+        var me = {};
+
+        /**
+         * hides all status messages no matter which module showed them
+         *
+         * @name   Controller.hideStatusMessages
+         * @function
+         */
+        me.hideStatusMessages = function()
+        {
+            PasteStatus.hideMessages();
+            Alert.hideMessages();
+        }
+
+        /**
+         * creates a new paste
+         *
+         * @name   Controller.newPaste
+         * @function
+         */
+        me.newPaste = function()
+        {
+            // 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');
+
+            PasteStatus.hideMessages();
+            PasteViewer.hide();
+            Editor.resetInput();
+            Editor.show();
+            Editor.focusInput();
+
+            TopNav.showCreateButtons();
+            Alert.hideLoading();
+        }
+
+        /**
+         * shows the loaded paste
+         *
+         * @name   Controller.showPaste
+         * @function
+         */
+        me.showPaste = function()
+        {
+            try {
+                Model.getPasteId();
+                Model.getPasteKey();
+            } catch (err) {
+                console.error(err);
+
+                // 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();
+            return;
+        }
+
+        /**
+         * refreshes the loaded paste to show potential new data
+         *
+         * @name   Controller.refreshPaste
+         * @function
+         * @param  {function} callback
+         */
+        me.refreshPaste = function(callback)
+        {
+            // save window position to restore it later
+            var orgPosition = $(window).scrollTop();
+
+            Uploader.prepare();
+            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
+
+            Uploader.setFailure(function (status, data) {
+                // revert loading status…
+                Alert.hideLoading();
+                TopNav.showViewButtons();
+
+                // 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);
+
+                callback();
+            })
+            Uploader.run();
+        }
+
+        /**
+         * clone the current paste
+         *
+         * @name   Controller.clonePaste
+         * @function
+         * @param  {Event} event
+         */
+        me.clonePaste = function(event)
+        {
+            TopNav.collapseBar();
+            TopNav.hideAllButtons();
+            Alert.showLoading('Cloning paste…', 0, 'transfer');
+
+            // hide messages from previous paste
+            me.hideStatusMessages();
+
+            // erase the id and the key in url
+            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
+
+            if (AttachmentViewer.hasAttachment()) {
+                AttachmentViewer.moveAttachmentTo(
+                    TopNav.getCustomAttachment(),
+                    'Cloned: \'%s\''
+                );
+                TopNav.hideFileSelector();
+                AttachmentViewer.hideAttachment();
+                // NOTE: it also looks nice without removing the attachment
+                // but for a consistent display we remove it…
+                AttachmentViewer.hideAttachmentPreview();
+                TopNav.showCustomAttachment();
+
+                // show another status message to make the user aware that the
+                // file was cloned too!
+                Alert.showStatus(
+                    [
+                        'The cloned file \'%s\' was attached to this paste.',
+                        AttachmentViewer.getAttachment()[1]
+                    ], 'copy', true, true);
+            }
+
+            Editor.setText(PasteViewer.getText())
+            PasteViewer.hide();
+            Editor.show();
+
+            Alert.hideLoading();
+            TopNav.showCreateButtons();
+        }
+
+        /**
+         * removes a saved paste
+         *
+         * @name   Controller.removePaste
+         * @function
+         * @param  {string} pasteId
+         * @param  {string} deleteToken
+         */
+        me.removePaste = function(pasteId, deleteToken) {
+            // unfortunately many web servers don't support DELETE (and PUT) out of the box
+            // so we use a POST request
+            Uploader.prepare();
+            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
+            Uploader.setUnencryptedData('deletetoken', deleteToken);
+
+            Uploader.setFailure(function () {
+                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
+            })
+            Uploader.run();
+        }
+
+        /**
+         * application start
+         *
+         * @name   Controller.init
+         * @function
+         */
+        me.init = function()
+        {
+            // first load translations
+            I18n.loadTranslations();
+
+            // initialize other modules/"classes"
+            Alert.init();
+            Model.init();
+
+            AttachmentViewer.init();
+            DiscussionViewer.init();
+            Editor.init();
+            PasteDecrypter.init();
+            PasteEncrypter.init();
+            PasteStatus.init();
+            PasteViewer.init();
+            Prompt.init();
+            TopNav.init();
+            UiHelper.init();
+            Uploader.init();
+
+            // display an existing paste
+            if (Model.hasCipherData()) {
+                return me.showPaste();
+            }
+
+            // otherwise create a new paste
+            me.newPaste();
+        }
+
+        return me;
+    })(window, document);
+
+    return {
+        Helper: Helper,
+        I18n: I18n,
+        CryptTool: CryptTool,
+        Model: Model,
+        UiHelper: UiHelper,
+        Alert: Alert,
+        PasteStatus: PasteStatus,
+        Prompt: Prompt,
+        Editor: Editor,
+        PasteViewer: PasteViewer,
+        AttachmentViewer: AttachmentViewer,
+        DiscussionViewer: DiscussionViewer,
+        TopNav: TopNav,
+        Uploader: Uploader,
+        PasteEncrypter: PasteEncrypter,
+        PasteDecrypter: PasteDecrypter,
+        Controller: Controller
+    };
+}(jQuery, sjcl, Base64, RawDeflate);

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
js/showdown-1.4.1.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
js/showdown-1.6.1.js


+ 0 - 58
js/sjcl-1.0.4.js

@@ -1,58 +0,0 @@
-"use strict";function q(a){throw a;}var s=void 0,u=!1;var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
-"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){return sjcl});
-sjcl.cipher.aes=function(a){this.l[0][0][0]||this.G();var b,c,d,e,g=this.l[0][4],f=this.l[1];b=a.length;var h=1;4!==b&&(6!==b&&8!==b)&&q(new sjcl.exception.invalid("invalid aes key size"));this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=g[c>>>24]<<24^g[c>>16&255]<<16^g[c>>8&255]<<8^g[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:f[0][g[c>>>24]]^f[1][g[c>>16&255]]^f[2][g[c>>8&255]]^f[3][g[c&
-255]]};
-sjcl.cipher.aes.prototype={encrypt:function(a){return w(this,a,0)},decrypt:function(a){return w(this,a,1)},l:[[[],[],[],[],[]],[[],[],[],[],[]]],G:function(){var a=this.l[0],b=this.l[1],c=a[4],d=b[4],e,g,f,h=[],l=[],k,m,n,p;for(e=0;0x100>e;e++)l[(h[e]=e<<1^283*(e>>7))^e]=e;for(g=f=0;!c[g];g^=k||1,f=l[f]||1){n=f^f<<1^f<<2^f<<3^f<<4;n=n>>8^n&255^99;c[g]=n;d[n]=g;m=h[e=h[k=h[g]]];p=0x1010101*m^0x10001*e^0x101*k^0x1010100*g;m=0x101*h[n]^0x1010100*n;for(e=0;4>e;e++)a[e][g]=m=m<<24^m>>>8,b[e][n]=p=p<<24^p>>>8}for(e=
-0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}};
-function w(a,b,c){4!==b.length&&q(new sjcl.exception.invalid("invalid aes block size"));var d=a.b[c],e=b[0]^d[0],g=b[c?3:1]^d[1],f=b[2]^d[2];b=b[c?1:3]^d[3];var h,l,k,m=d.length/4-2,n,p=4,t=[0,0,0,0];h=a.l[c];a=h[0];var r=h[1],v=h[2],y=h[3],z=h[4];for(n=0;n<m;n++)h=a[e>>>24]^r[g>>16&255]^v[f>>8&255]^y[b&255]^d[p],l=a[g>>>24]^r[f>>16&255]^v[b>>8&255]^y[e&255]^d[p+1],k=a[f>>>24]^r[b>>16&255]^v[e>>8&255]^y[g&255]^d[p+2],b=a[b>>>24]^r[e>>16&255]^v[g>>8&255]^y[f&255]^d[p+3],p+=4,e=h,g=l,f=k;for(n=0;4>
-n;n++)t[c?3&-n:n]=z[e>>>24]<<24^z[g>>16&255]<<16^z[f>>8&255]<<8^z[b&255]^d[p++],h=e,e=g,g=f,f=b,b=h;return t}
-sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.R(a.slice(b/32),32-(b&31)).slice(1);return c===s?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(0===a.length||0===b.length)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return 32===d?a.concat(b):sjcl.bitArray.R(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;return 0===
-b?0:32*(b-1)+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(32*a.length<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b&=31;0<c&&b&&(a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return u;var c=0,d;for(d=0;d<a.length;d++)c|=a[d]^b[d];return 0===
-c},R:function(a,b,c,d){var e;e=0;for(d===s&&(d=[]);32<=b;b-=32)d.push(c),c=0;if(0===b)return d.concat(a);for(e=0;e<a.length;e++)d.push(c|a[e]>>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32<b+a?c:d.pop(),1));return d},g:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]},byteswapM:function(a){var b,c;for(b=0;b<a.length;++b)c=a[b],a[b]=c>>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}};
-sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++)0===(d&3)&&(e=a[d/4]),b+=String.fromCharCode(e>>>24),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++)d=d<<8|a.charCodeAt(c),3===(c&3)&&(b.push(d),d=0);c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
-sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a+="00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,4*d)}};
-sjcl.codec.base32={p:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",O:"0123456789ABCDEFGHIJKLMNOPQRSTUV",BITS:32,BASE:5,REMAINING:27,fromBits:function(a,b,c){var d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,g="",f=0,h=sjcl.codec.base32.p,l=0,k=sjcl.bitArray.bitLength(a);c&&(h=sjcl.codec.base32.O);for(c=0;g.length*d<k;)g+=h.charAt((l^a[c]>>>f)>>>e),f<d?(l=a[c]<<d-f,f+=e,c++):(l<<=d,f-=d);for(;g.length&7&&!b;)g+="=";return g},toBits:function(a,b){a=a.replace(/\s|=/g,"").toUpperCase();var c=sjcl.codec.base32.BITS,
-d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,g=[],f,h=0,l=sjcl.codec.base32.p,k=0,m,n="base32";b&&(l=sjcl.codec.base32.O,n="base32hex");for(f=0;f<a.length;f++){m=l.indexOf(a.charAt(f));if(0>m){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}q(new sjcl.exception.invalid("this isn't "+n+"!"))}h>e?(h-=e,g.push(k^m>>>h),k=m<<c-h):(h+=d,k^=m<<c-h)}h&56&&g.push(sjcl.bitArray.partial(h&56,k,1));return g}};
-sjcl.codec.base32hex={fromBits:function(a,b){return sjcl.codec.base32.fromBits(a,b,1)},toBits:function(a){return sjcl.codec.base32.toBits(a,1)}};
-sjcl.codec.base64={p:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,g=sjcl.codec.base64.p,f=0,h=sjcl.bitArray.bitLength(a);c&&(g=g.substr(0,62)+"-_");for(c=0;6*d.length<h;)d+=g.charAt((f^a[c]>>>e)>>>26),6>e?(f=a[c]<<6-e,e+=26,c++):(f<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,g=sjcl.codec.base64.p,f=0,h;b&&(g=g.substr(0,62)+"-_");for(d=0;d<a.length;d++)h=g.indexOf(a.charAt(d)),
-0>h&&q(new sjcl.exception.invalid("this isn't base64!")),26<e?(e-=26,c.push(f^h>>>e),f=h<<32-e):(e+=6,f^=h<<32-e);e&56&&c.push(sjcl.bitArray.partial(e&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.G();a?(this.s=a.s.slice(0),this.o=a.o.slice(0),this.i=a.i):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
-sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.s=this.P.slice(0);this.o=[];this.i=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.o=sjcl.bitArray.concat(this.o,a);b=this.i;a=this.i=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)x(this,c.splice(0,16));return this},finalize:function(){var a,b=this.o,c=this.s,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.i/
-4294967296));for(b.push(this.i|0);b.length;)x(this,b.splice(0,16));this.reset();return c},P:[],b:[],G:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}var b=0,c=2,d;a:for(;64>b;c++){for(d=2;d*d<=c;d++)if(0===c%d)continue a;8>b&&(this.P[b]=a(Math.pow(c,0.5)));this.b[b]=a(Math.pow(c,1/3));b++}}};
-function x(a,b){var c,d,e,g=b.slice(0),f=a.s,h=a.b,l=f[0],k=f[1],m=f[2],n=f[3],p=f[4],t=f[5],r=f[6],v=f[7];for(c=0;64>c;c++)16>c?d=g[c]:(d=g[c+1&15],e=g[c+14&15],d=g[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+g[c&15]+g[c+9&15]|0),d=d+v+(p>>>6^p>>>11^p>>>25^p<<26^p<<21^p<<7)+(r^p&(t^r))+h[c],v=r,r=t,t=p,p=n+d|0,n=m,m=k,k=l,l=d+(k&m^n&(k^m))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+l|0;f[1]=f[1]+k|0;f[2]=f[2]+m|0;f[3]=f[3]+n|0;f[4]=f[4]+p|0;f[5]=f[5]+t|0;f[6]=
-f[6]+r|0;f[7]=f[7]+v|0}
-sjcl.mode.ccm={name:"ccm",t:[],listenProgress:function(a){sjcl.mode.ccm.t.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.t.indexOf(a);-1<a&&sjcl.mode.ccm.t.splice(a,1)},X:function(a){var b=sjcl.mode.ccm.t.slice(),c;for(c=0;c<b.length;c+=1)b[c](a)},encrypt:function(a,b,c,d,e){var g,f=b.slice(0),h=sjcl.bitArray,l=h.bitLength(c)/8,k=h.bitLength(f)/8;e=e||64;d=d||[];7>l&&q(new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"));for(g=2;4>g&&k>>>8*g;g++);g<15-l&&(g=15-l);c=h.clamp(c,8*(15-
-g));b=sjcl.mode.ccm.M(a,b,c,d,e,g);f=sjcl.mode.ccm.q(a,f,c,b,e,g);return h.concat(f.data,f.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var g=sjcl.bitArray,f=g.bitLength(c)/8,h=g.bitLength(b),l=g.clamp(b,h-e),k=g.bitSlice(b,h-e),h=(h-e)/8;7>f&&q(new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"));for(b=2;4>b&&h>>>8*b;b++);b<15-f&&(b=15-f);c=g.clamp(c,8*(15-b));l=sjcl.mode.ccm.q(a,l,c,k,e,b);a=sjcl.mode.ccm.M(a,l.data,c,d,e,b);g.equal(l.tag,a)||q(new sjcl.exception.corrupt("ccm: tag doesn't match"));
-return l.data},ea:function(a,b,c,d,e,g){var f=[],h=sjcl.bitArray,l=h.g;d=[h.partial(8,(b.length?64:0)|d-2<<2|g-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length){c=h.bitLength(b)/8;65279>=c?f=[h.partial(16,c)]:0xffffffff>=c&&(f=h.concat([h.partial(16,65534)],[c]));f=h.concat(f,b);for(b=0;b<f.length;b+=4)d=a.encrypt(l(d,f.slice(b,b+4).concat([0,0,0])))}return d},M:function(a,b,c,d,e,g){var f=sjcl.bitArray,h=f.g;e/=8;(e%2||4>e||16<e)&&q(new sjcl.exception.invalid("ccm: invalid tag length"));(0xffffffff<
-d.length||0xffffffff<b.length)&&q(new sjcl.exception.bug("ccm: can't deal with 4GiB or more data"));c=sjcl.mode.ccm.ea(a,d,c,e,f.bitLength(b)/8,g);for(d=0;d<b.length;d+=4)c=a.encrypt(h(c,b.slice(d,d+4).concat([0,0,0])));return f.clamp(c,8*e)},q:function(a,b,c,d,e,g){var f,h=sjcl.bitArray;f=h.g;var l=b.length,k=h.bitLength(b),m=l/50,n=m;c=h.concat([h.partial(8,g-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(f(d,a.encrypt(c)),0,e);if(!l)return{tag:d,data:[]};for(f=0;f<l;f+=4)f>m&&(sjcl.mode.ccm.X(f/
-l),m+=n),c[3]++,e=a.encrypt(c),b[f]^=e[0],b[f+1]^=e[1],b[f+2]^=e[2],b[f+3]^=e[3];return{tag:d,data:h.clamp(b,k)}}};
-sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,g){128!==sjcl.bitArray.bitLength(c)&&q(new sjcl.exception.invalid("ocb iv must be 128 bits"));var f,h=sjcl.mode.ocb2.J,l=sjcl.bitArray,k=l.g,m=[0,0,0,0];c=h(a.encrypt(c));var n,p=[];d=d||[];e=e||64;for(f=0;f+4<b.length;f+=4)n=b.slice(f,f+4),m=k(m,n),p=p.concat(k(c,a.encrypt(k(c,n)))),c=h(c);n=b.slice(f);b=l.bitLength(n);f=a.encrypt(k(c,[0,0,0,b]));n=l.clamp(k(n.concat([0,0,0]),f),b);m=k(m,k(n.concat([0,0,0]),f));m=a.encrypt(k(m,k(c,h(c))));d.length&&
-(m=k(m,g?d:sjcl.mode.ocb2.pmac(a,d)));return p.concat(l.concat(n,l.clamp(m,e)))},decrypt:function(a,b,c,d,e,g){128!==sjcl.bitArray.bitLength(c)&&q(new sjcl.exception.invalid("ocb iv must be 128 bits"));e=e||64;var f=sjcl.mode.ocb2.J,h=sjcl.bitArray,l=h.g,k=[0,0,0,0],m=f(a.encrypt(c)),n,p,t=sjcl.bitArray.bitLength(b)-e,r=[];d=d||[];for(c=0;c+4<t/32;c+=4)n=l(m,a.decrypt(l(m,b.slice(c,c+4)))),k=l(k,n),r=r.concat(n),m=f(m);p=t-32*c;n=a.encrypt(l(m,[0,0,0,p]));n=l(n,h.clamp(b.slice(c),p).concat([0,0,0]));
-k=l(k,n);k=a.encrypt(l(k,l(m,f(m))));d.length&&(k=l(k,g?d:sjcl.mode.ocb2.pmac(a,d)));h.equal(h.clamp(k,e),h.bitSlice(b,t))||q(new sjcl.exception.corrupt("ocb: tag doesn't match"));return r.concat(h.clamp(n,p))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.J,e=sjcl.bitArray,g=e.g,f=[0,0,0,0],h=a.encrypt([0,0,0,0]),h=g(h,d(d(h)));for(c=0;c+4<b.length;c+=4)h=d(h),f=g(f,a.encrypt(g(h,b.slice(c,c+4))));c=b.slice(c);128>e.bitLength(c)&&(h=g(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));f=g(f,c);return a.encrypt(g(d(g(h,
-d(h))),f))},J:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}};
-sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var g=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.q(!0,a,g,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var g=b.slice(0),f=sjcl.bitArray,h=f.bitLength(g);e=e||128;d=d||[];e<=h?(b=f.bitSlice(g,h-e),g=f.bitSlice(g,0,h-e)):(b=g,g=[]);a=sjcl.mode.gcm.q(u,a,g,d,c,e);f.equal(a.tag,b)||q(new sjcl.exception.corrupt("gcm: tag doesn't match"));return a.data},ba:function(a,b){var c,d,e,g,f,h=sjcl.bitArray.g;e=[0,0,0,0];g=
-b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,g));f=0!==(g[3]&1);for(d=3;0<d;d--)g[d]=g[d]>>>1|(g[d-1]&1)<<31;g[0]>>>=1;f&&(g[0]^=-0x1f000000)}return e},h:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;d<e;d+=4)b[0]^=0xffffffff&c[d],b[1]^=0xffffffff&c[d+1],b[2]^=0xffffffff&c[d+2],b[3]^=0xffffffff&c[d+3],b=sjcl.mode.gcm.ba(b,a);return b},q:function(a,b,c,d,e,g){var f,h,l,k,m,n,p,t,r=sjcl.bitArray;n=c.length;p=r.bitLength(c);t=r.bitLength(d);h=r.bitLength(e);f=
-b.encrypt([0,0,0,0]);96===h?(e=e.slice(0),e=r.concat(e,[1])):(e=sjcl.mode.gcm.h(f,[0,0,0,0],e),e=sjcl.mode.gcm.h(f,e,[0,0,Math.floor(h/0x100000000),h&0xffffffff]));h=sjcl.mode.gcm.h(f,[0,0,0,0],d);m=e.slice(0);d=h.slice(0);a||(d=sjcl.mode.gcm.h(f,h,c));for(k=0;k<n;k+=4)m[3]++,l=b.encrypt(m),c[k]^=l[0],c[k+1]^=l[1],c[k+2]^=l[2],c[k+3]^=l[3];c=r.clamp(c,p);a&&(d=sjcl.mode.gcm.h(f,h,c));a=[Math.floor(t/0x100000000),t&0xffffffff,Math.floor(p/0x100000000),p&0xffffffff];d=sjcl.mode.gcm.h(f,d,a);l=b.encrypt(e);
-d[0]^=l[0];d[1]^=l[1];d[2]^=l[2];d[3]^=l[3];return{tag:r.bitSlice(d,0,g),data:c}}};sjcl.misc.hmac=function(a,b){this.N=b=b||sjcl.hash.sha256;var c=[[],[]],d,e=b.prototype.blockSize/32;this.n=[new b,new b];a.length>e&&(a=b.hash(a));for(d=0;d<e;d++)c[0][d]=a[d]^909522486,c[1][d]=a[d]^1549556828;this.n[0].update(c[0]);this.n[1].update(c[1]);this.I=new b(this.n[0])};
-sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a){this.S&&q(new sjcl.exception.invalid("encrypt on already updated hmac called!"));this.update(a);return this.digest(a)};sjcl.misc.hmac.prototype.reset=function(){this.I=new this.N(this.n[0]);this.S=u};sjcl.misc.hmac.prototype.update=function(a){this.S=!0;this.I.update(a)};sjcl.misc.hmac.prototype.digest=function(){var a=this.I.finalize(),a=(new this.N(this.n[1])).update(a).finalize();this.reset();return a};
-sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E3;(0>d||0>c)&&q(sjcl.exception.invalid("invalid params to pbkdf2"));"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var g,f,h,l,k=[],m=sjcl.bitArray;for(l=1;32*k.length<(d||1);l++){e=g=a.encrypt(m.concat(b,[l]));for(f=1;f<c;f++){g=a.encrypt(g);for(h=0;h<g.length;h++)e[h]^=g[h]}k=k.concat(e)}d&&(k=m.clamp(k,d));return k};
-sjcl.prng=function(a){this.c=[new sjcl.hash.sha256];this.j=[0];this.H=0;this.u={};this.F=0;this.L={};this.Q=this.d=this.k=this.Z=0;this.b=[0,0,0,0,0,0,0,0];this.f=[0,0,0,0];this.C=s;this.D=a;this.r=u;this.B={progress:{},seeded:{}};this.m=this.Y=0;this.w=1;this.A=2;this.U=0x10000;this.K=[0,48,64,96,128,192,0x100,384,512,768,1024];this.V=3E4;this.T=80};
-sjcl.prng.prototype={randomWords:function(a,b){var c=[],d;d=this.isReady(b);var e;d===this.m&&q(new sjcl.exception.notReady("generator isn't seeded"));if(d&this.A){d=!(d&this.w);e=[];var g=0,f;this.Q=e[0]=(new Date).valueOf()+this.V;for(f=0;16>f;f++)e.push(0x100000000*Math.random()|0);for(f=0;f<this.c.length&&!(e=e.concat(this.c[f].finalize()),g+=this.j[f],this.j[f]=0,!d&&this.H&1<<f);f++);this.H>=1<<this.c.length&&(this.c.push(new sjcl.hash.sha256),this.j.push(0));this.d-=g;g>this.k&&(this.k=g);this.H++;
-this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.C=new sjcl.cipher.aes(this.b);for(d=0;4>d&&!(this.f[d]=this.f[d]+1|0,this.f[d]);d++);}for(d=0;d<a;d+=4)0===(d+1)%this.U&&A(this),e=B(this),c.push(e[0],e[1],e[2],e[3]);A(this);return c.slice(0,a)},setDefaultParanoia:function(a,b){0===a&&"Setting paranoia=0 will ruin your security; use it only for testing"!==b&&q("Setting paranoia=0 will ruin your security; use it only for testing");this.D=a},addEntropy:function(a,b,c){c=c||"user";var d,e,g=(new Date).valueOf(),
-f=this.u[c],h=this.isReady(),l=0;d=this.L[c];d===s&&(d=this.L[c]=this.Z++);f===s&&(f=this.u[c]=0);this.u[c]=(this.u[c]+1)%this.c.length;switch(typeof a){case "number":b===s&&(b=1);this.c[f].update([d,this.F++,1,b,g,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if("[object Uint32Array]"===c){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else{"[object Array]"!==c&&(l=1);for(c=0;c<a.length&&!l;c++)"number"!==typeof a[c]&&(l=1)}if(!l){if(b===s)for(c=b=0;c<a.length;c++)for(e=a[c];0<e;)b++,
-e>>>=1;this.c[f].update([d,this.F++,2,b,g,a.length].concat(a))}break;case "string":b===s&&(b=a.length);this.c[f].update([d,this.F++,3,b,g,a.length]);this.c[f].update(a);break;default:l=1}l&&q(new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string"));this.j[f]+=b;this.d+=b;h===this.m&&(this.isReady()!==this.m&&C("seeded",Math.max(this.k,this.d)),C("progress",this.getProgress()))},isReady:function(a){a=this.K[a!==s?a:this.D];return this.k&&this.k>=a?this.j[0]>this.T&&
-(new Date).valueOf()>this.Q?this.A|this.w:this.w:this.d>=a?this.A|this.m:this.m},getProgress:function(a){a=this.K[a?a:this.D];return this.k>=a?1:this.d>a?1:this.d/a},startCollectors:function(){this.r||(this.a={loadTimeCollector:D(this,this.da),mouseCollector:D(this,this.fa),keyboardCollector:D(this,this.ca),accelerometerCollector:D(this,this.W),touchCollector:D(this,this.ha)},window.addEventListener?(window.addEventListener("load",this.a.loadTimeCollector,u),window.addEventListener("mousemove",this.a.mouseCollector,
-u),window.addEventListener("keypress",this.a.keyboardCollector,u),window.addEventListener("devicemotion",this.a.accelerometerCollector,u),window.addEventListener("touchmove",this.a.touchCollector,u)):document.attachEvent?(document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector)):q(new sjcl.exception.bug("can't attach event")),this.r=!0)},stopCollectors:function(){this.r&&(window.removeEventListener?
-(window.removeEventListener("load",this.a.loadTimeCollector,u),window.removeEventListener("mousemove",this.a.mouseCollector,u),window.removeEventListener("keypress",this.a.keyboardCollector,u),window.removeEventListener("devicemotion",this.a.accelerometerCollector,u),window.removeEventListener("touchmove",this.a.touchCollector,u)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove",this.a.mouseCollector),document.detachEvent("keypress",
-this.a.keyboardCollector)),this.r=u)},addEventListener:function(a,b){this.B[a][this.Y++]=b},removeEventListener:function(a,b){var c,d,e=this.B[a],g=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&g.push(d);for(c=0;c<g.length;c++)d=g[c],delete e[d]},ca:function(){E(1)},fa:function(a){var b,c;try{b=a.x||a.clientX||a.offsetX||0,c=a.y||a.clientY||a.offsetY||0}catch(d){c=b=0}0!=b&&0!=c&&sjcl.random.addEntropy([b,c],2,"mouse");E(0)},ha:function(a){a=a.touches[0]||a.changedTouches[0];sjcl.random.addEntropy([a.pageX||
-a.clientX,a.pageY||a.clientY],1,"touch");E(0)},da:function(){E(2)},W:function(a){a=a.accelerationIncludingGravity.x||a.accelerationIncludingGravity.y||a.accelerationIncludingGravity.z;if(window.orientation){var b=window.orientation;"number"===typeof b&&sjcl.random.addEntropy(b,1,"accelerometer")}a&&sjcl.random.addEntropy(a,2,"accelerometer");E(0)}};function C(a,b){var c,d=sjcl.random.B[a],e=[];for(c in d)d.hasOwnProperty(c)&&e.push(d[c]);for(c=0;c<e.length;c++)e[c](b)}
-function E(a){"undefined"!==typeof window&&window.performance&&"function"===typeof window.performance.now?sjcl.random.addEntropy(window.performance.now(),a,"loadtime"):sjcl.random.addEntropy((new Date).valueOf(),a,"loadtime")}function A(a){a.b=B(a).concat(B(a));a.C=new sjcl.cipher.aes(a.b)}function B(a){for(var b=0;4>b&&!(a.f[b]=a.f[b]+1|0,a.f[b]);b++);return a.C.encrypt(a.f)}function D(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6);
-a:try{var F,G,H,I;if(I="undefined"!==typeof module){var J;if(J=module.exports){var K;try{K=require("crypto")}catch(L){K=null}J=(G=K)&&G.randomBytes}I=J}if(I)F=G.randomBytes(128),F=new Uint32Array((new Uint8Array(F)).buffer),sjcl.random.addEntropy(F,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){H=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(H);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(H);
-else break a;sjcl.random.addEntropy(H,1024,"crypto['getRandomValues']")}}catch(M){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(M))}
-sjcl.json={defaults:{v:1,iter:1E3,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},aa:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,g=e.e({iv:sjcl.random.randomWords(4,0)},e.defaults),f;e.e(g,c);c=g.adata;"string"===typeof g.salt&&(g.salt=sjcl.codec.base64.toBits(g.salt));"string"===typeof g.iv&&(g.iv=sjcl.codec.base64.toBits(g.iv));(!sjcl.mode[g.mode]||!sjcl.cipher[g.cipher]||"string"===typeof a&&100>=g.iter||64!==g.ts&&96!==g.ts&&128!==g.ts||128!==g.ks&&192!==g.ks&&0x100!==g.ks||2>g.iv.length||
-4<g.iv.length)&&q(new sjcl.exception.invalid("json encrypt: invalid parameters"));"string"===typeof a?(f=sjcl.misc.cachedPbkdf2(a,g),a=f.key.slice(0,g.ks/32),g.salt=f.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.publicKey&&(f=a.kem(),g.kemtag=f.tag,a=f.key.slice(0,g.ks/32));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));"string"===typeof c&&(g.adata=c=sjcl.codec.utf8String.toBits(c));f=new sjcl.cipher[g.cipher](a);e.e(d,g);d.key=a;g.ct="ccm"===g.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&
-b instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.encrypt(f,b,g.iv,c,g.ts):sjcl.mode[g.mode].encrypt(f,b,g.iv,c,g.ts);return g},encrypt:function(a,b,c,d){var e=sjcl.json,g=e.aa.apply(e,arguments);return e.encode(g)},$:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.e(e.e(e.e({},e.defaults),b),c,!0);var g,f;g=b.adata;"string"===typeof b.salt&&(b.salt=sjcl.codec.base64.toBits(b.salt));"string"===typeof b.iv&&(b.iv=sjcl.codec.base64.toBits(b.iv));(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||"string"===
-typeof a&&100>=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4<b.iv.length)&&q(new sjcl.exception.invalid("json decrypt: invalid parameters"));"string"===typeof a?(f=sjcl.misc.cachedPbkdf2(a,b),a=f.key.slice(0,b.ks/32),b.salt=f.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.secretKey&&(a=a.unkem(sjcl.codec.base64.toBits(b.kemtag)).slice(0,b.ks/32));"string"===typeof g&&(g=sjcl.codec.utf8String.toBits(g));f=new sjcl.cipher[b.cipher](a);g="ccm"===
-b.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&b.ct instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.decrypt(f,b.ct,b.iv,b.tag,g,b.ts):sjcl.mode[b.mode].decrypt(f,b.ct,b.iv,g,b.ts);e.e(d,b);d.key=a;return 1===c.raw?g:sjcl.codec.utf8String.fromBits(g)},decrypt:function(a,b,c,d){var e=sjcl.json;return e.$(a,e.decode(b),c,d)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b))switch(b.match(/^[a-z0-9]+$/i)||q(new sjcl.exception.invalid("json encode: invalid property name")),c+=d+'"'+b+
-'":',d=",",typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],0)+'"';break;default:q(new sjcl.exception.bug("json encode: unsupported type"))}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");a.match(/^\{.*\}$/)||q(new sjcl.exception.invalid("json decode: this isn't json!"));a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++)(d=a[c].match(/^\s*(?:(["']?)([a-z][a-z0-9]*)\1)\s*:\s*(?:(-?\d+)|"([a-z0-9+\/%*_.@=\-]*)"|(true|false))$/i))||
-q(new sjcl.exception.invalid("json decode: this isn't json!")),null!=d[3]?b[d[2]]=parseInt(d[3],10):null!=d[4]?b[d[2]]=d[2].match(/^(ct|adata|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4]):null!=d[5]&&(b[d[2]]="true"===d[5]);return b},e:function(a,b,c){a===s&&(a={});if(b===s)return a;for(var d in b)b.hasOwnProperty(d)&&(c&&(a[d]!==s&&a[d]!==b[d])&&q(new sjcl.exception.invalid("required parameter overridden")),a[d]=b[d]);return a},ja:function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&
-a[d]!==b[d]&&(c[d]=a[d]);return c},ia:function(a,b){var c={},d;for(d=0;d<b.length;d++)a[b[d]]!==s&&(c[b[d]]=a[b[d]]);return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.ga={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.ga,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=b.salt===s?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};

+ 60 - 0
js/sjcl-1.0.6.js

@@ -0,0 +1,60 @@
+"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
+sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c&
+255]]};
+sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e=
+0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}};
+function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m<n;m++)h=a[e>>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m=
+0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r}
+sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(0===a.length||0===b.length)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return 32===d?a.concat(b):sjcl.bitArray.$(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;return 0===
+b?0:32*(b-1)+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(32*a.length<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b=b&31;0<c&&b&&(a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d<a.length;d++)c|=a[d]^b[d];return 0===
+c},$:function(a,b,c,d){var e;e=0;for(void 0===d&&(d=[]);32<=b;b-=32)d.push(c),c=0;if(0===b)return d.concat(a);for(e=0;e<a.length;e++)d.push(c|a[e]>>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32<b+a?c:d.pop(),1));return d},i:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]},byteswapM:function(a){var b,c;for(b=0;b<a.length;++b)c=a[b],a[b]=c>>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}};
+sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++)0===(d&3)&&(e=a[d/4]),b+=String.fromCharCode(e>>>24),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++)d=d<<8|a.charCodeAt(c),3===(c&3)&&(b.push(d),d=0);c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
+sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a=a+"00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,4*d)}};
+sjcl.codec.base32={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",X:"0123456789ABCDEFGHIJKLMNOPQRSTUV",BITS:32,BASE:5,REMAINING:27,fromBits:function(a,b,c){var d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f="",g=0,h=sjcl.codec.base32.B,k=0,l=sjcl.bitArray.bitLength(a);c&&(h=sjcl.codec.base32.X);for(c=0;f.length*d<l;)f+=h.charAt((k^a[c]>>>g)>>>e),g<d?(k=a[c]<<d-g,g+=e,c++):(k<<=d,g-=d);for(;f.length&7&&!b;)f+="=";return f},toBits:function(a,b){a=a.replace(/\s|=/g,"").toUpperCase();var c=sjcl.codec.base32.BITS,
+d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f=[],g,h=0,k=sjcl.codec.base32.B,l=0,n,m="base32";b&&(k=sjcl.codec.base32.X,m="base32hex");for(g=0;g<a.length;g++){n=k.indexOf(a.charAt(g));if(0>n){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<<c-h):(h+=d,l^=n<<c-h)}h&56&&f.push(sjcl.bitArray.partial(h&56,l,1));return f}};
+sjcl.codec.base32hex={fromBits:function(a,b){return sjcl.codec.base32.fromBits(a,b,1)},toBits:function(a){return sjcl.codec.base32.toBits(a,1)}};
+sjcl.codec.base64={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.B,g=0,h=sjcl.bitArray.bitLength(a);c&&(f=f.substr(0,62)+"-_");for(c=0;6*d.length<h;)d+=f.charAt((g^a[c]>>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;d<a.length;d++){h=f.indexOf(a.charAt(d));
+if(0>h)throw new sjcl.exception.invalid("this isn't base64!");26<e?(e-=26,c.push(g^h>>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
+sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffff<a)throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits");if("undefined"!==typeof Uint32Array){var d=new Uint32Array(c),e=0;for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,d.subarray(16*e,
+16*(e+1))),e+=1;c.splice(0,16*e)}else for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,c.splice(0,16));return this},finalize:function(){var a,b=this.A,c=this.F,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.l/0x100000000));for(b.push(this.l|0);b.length;)u(this,b.splice(0,16));this.reset();return c},Y:[],b:[],O:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}for(var b=0,c=2,d,e;64>b;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e=
+!1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}};
+function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]=
+f[7]+q|0}
+sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1<a&&sjcl.mode.ccm.G.splice(a,1)},fa:function(a){var b=sjcl.mode.ccm.G.slice(),c;for(c=0;c<b.length;c+=1)b[c](a)},encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,k=h.bitLength(c)/8,l=h.bitLength(g)/8;e=e||64;d=d||[];if(7>k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c,
+8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");
+return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;b<g.length;b+=4)d=a.encrypt(k(d,g.slice(b,b+4).concat([0,0,0])));return d},V:function(a,b,c,d,e,f){var g=sjcl.bitArray,h=g.i;e/=8;if(e%2||4>e||16<e)throw new sjcl.exception.invalid("ccm: invalid tag length");
+if(0xffffffff<d.length||0xffffffff<b.length)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");c=sjcl.mode.ccm.na(a,d,c,e,g.bitLength(b)/8,f);for(d=0;d<b.length;d+=4)c=a.encrypt(h(c,b.slice(d,d+4).concat([0,0,0])));return g.clamp(c,8*e)},C:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.i;var k=b.length,l=h.bitLength(b),n=k/50,m=n;c=h.concat([h.partial(8,f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!k)return{tag:d,data:[]};for(g=0;g<k;g+=4)g>n&&(sjcl.mode.ccm.fa(g/
+k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}};
+sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4)m=b.slice(g,g+4),n=l(n,m),p=p.concat(l(c,a.encrypt(l(c,m)))),c=h(c);m=b.slice(g);b=k.bitLength(m);g=a.encrypt(l(c,[0,0,0,b]));m=k.clamp(l(m.concat([0,0,0]),g),b);n=l(n,l(m.concat([0,0,0]),g));n=a.encrypt(l(n,l(c,h(c))));
+d.length&&(n=l(n,f?d:sjcl.mode.ocb2.pmac(a,d)));return p.concat(k.concat(m,k.clamp(n,e)))},decrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.S,h=sjcl.bitArray,k=h.i,l=[0,0,0,0],n=g(a.encrypt(c)),m,p,r=sjcl.bitArray.bitLength(b)-e,q=[];d=d||[];for(c=0;c+4<r/32;c+=4)m=k(n,a.decrypt(k(n,b.slice(c,c+4)))),l=k(l,m),q=q.concat(m),n=g(n);p=r-32*c;m=a.encrypt(k(n,[0,0,0,p]));m=k(m,h.clamp(b.slice(c),p).concat([0,
+0,0]));l=k(l,m);l=a.encrypt(k(l,k(n,g(n))));d.length&&(l=k(l,f?d:sjcl.mode.ocb2.pmac(a,d)));if(!h.equal(h.clamp(l,e),h.bitSlice(b,r)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return q.concat(h.clamp(m,p))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.S,e=sjcl.bitArray,f=e.i,g=[0,0,0,0],h=a.encrypt([0,0,0,0]),h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4)h=d(h),g=f(g,a.encrypt(f(h,b.slice(c,c+4))));c=b.slice(c);128>e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c);
+return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}};
+sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0,
+0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0<d;d--)f[d]=f[d]>>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;d<e;d+=4)b[0]^=0xffffffff&c[d],b[1]^=0xffffffff&c[d+1],b[2]^=0xffffffff&c[d+2],b[3]^=0xffffffff&c[d+3],b=sjcl.mode.gcm.ka(b,a);return b},C:function(a,b,c,d,e,f){var g,h,k,l,n,m,p,r,q=sjcl.bitArray;m=c.length;p=q.bitLength(c);r=q.bitLength(d);h=q.bitLength(e);
+g=b.encrypt([0,0,0,0]);96===h?(e=e.slice(0),e=q.concat(e,[1])):(e=sjcl.mode.gcm.j(g,[0,0,0,0],e),e=sjcl.mode.gcm.j(g,e,[0,0,Math.floor(h/0x100000000),h&0xffffffff]));h=sjcl.mode.gcm.j(g,[0,0,0,0],d);n=e.slice(0);d=h.slice(0);a||(d=sjcl.mode.gcm.j(g,h,c));for(l=0;l<m;l+=4)n[3]++,k=b.encrypt(n),c[l]^=k[0],c[l+1]^=k[1],c[l+2]^=k[2],c[l+3]^=k[3];c=q.clamp(c,p);a&&(d=sjcl.mode.gcm.j(g,h,c));a=[Math.floor(r/0x100000000),r&0xffffffff,Math.floor(p/0x100000000),p&0xffffffff];d=sjcl.mode.gcm.j(g,d,a);k=b.encrypt(e);
+d[0]^=k[0];d[1]^=k[1];d[2]^=k[2];d[3]^=k[3];return{tag:q.bitSlice(d,0,f),data:c}}};sjcl.misc.hmac=function(a,b){this.W=b=b||sjcl.hash.sha256;var c=[[],[]],d,e=b.prototype.blockSize/32;this.w=[new b,new b];a.length>e&&(a=b.hash(a));for(d=0;d<e;d++)c[0][d]=a[d]^909522486,c[1][d]=a[d]^1549556828;this.w[0].update(c[0]);this.w[1].update(c[1]);this.R=new b(this.w[0])};
+sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a){if(this.aa)throw new sjcl.exception.invalid("encrypt on already updated hmac called!");this.update(a);return this.digest(a)};sjcl.misc.hmac.prototype.reset=function(){this.R=new this.W(this.w[0]);this.aa=!1};sjcl.misc.hmac.prototype.update=function(a){this.aa=!0;this.R.update(a)};sjcl.misc.hmac.prototype.digest=function(){var a=this.R.finalize(),a=(new this.W(this.w[1])).update(a).finalize();this.reset();return a};
+sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E4;if(0>d||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;g<c;g++)for(f=a.encrypt(f),h=0;h<f.length;h++)e[h]^=f[h];l=l.concat(e)}d&&(l=n.clamp(l,d));return l};
+sjcl.prng=function(a){this.c=[new sjcl.hash.sha256];this.m=[0];this.P=0;this.H={};this.N=0;this.U={};this.Z=this.f=this.o=this.ha=0;this.b=[0,0,0,0,0,0,0,0];this.h=[0,0,0,0];this.L=void 0;this.M=a;this.D=!1;this.K={progress:{},seeded:{}};this.u=this.ga=0;this.I=1;this.J=2;this.ca=0x10000;this.T=[0,48,64,96,128,192,0x100,384,512,768,1024];this.da=3E4;this.ba=80};
+sjcl.prng.prototype={randomWords:function(a,b){var c=[],d;d=this.isReady(b);var e;if(d===this.u)throw new sjcl.exception.notReady("generator isn't seeded");if(d&this.J){d=!(d&this.I);e=[];var f=0,g;this.Z=e[0]=(new Date).valueOf()+this.da;for(g=0;16>g;g++)e.push(0x100000000*Math.random()|0);for(g=0;g<this.c.length&&(e=e.concat(this.c[g].finalize()),f+=this.m[g],this.m[g]=0,d||!(this.P&1<<g));g++);this.P>=1<<this.c.length&&(this.c.push(new sjcl.hash.sha256),this.m.push(0));this.f-=f;f>this.o&&(this.o=
+f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d<a;d+=4)0===(d+1)%this.ca&&y(this),e=z(this),c.push(e[0],e[1],e[2],e[3]);y(this);return c.slice(0,a)},setDefaultParanoia:function(a,b){if(0===a&&"Setting paranoia=0 will ruin your security; use it only for testing"!==b)throw new sjcl.exception.invalid("Setting paranoia=0 will ruin your security; use it only for testing");this.M=a},addEntropy:function(a,
+b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.H[c],h=this.isReady(),k=0;d=this.U[c];void 0===d&&(d=this.U[c]=this.ha++);void 0===g&&(g=this.H[c]=0);this.H[c]=(this.H[c]+1)%this.c.length;switch(typeof a){case "number":void 0===b&&(b=1);this.c[g].update([d,this.N++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if("[object Uint32Array]"===c){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else for("[object Array]"!==c&&(k=1),c=0;c<a.length&&!k;c++)"number"!==typeof a[c]&&
+(k=1);if(!k){if(void 0===b)for(c=b=0;c<a.length;c++)for(e=a[c];0<e;)b++,e=e>>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))},
+isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load",
+this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event");
+this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove",
+this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;c<f.length;c++)d=f[c],delete e[d]},la:function(){C(this,1)},oa:function(a){var b,c;try{b=a.x||a.clientX||a.offsetX||0,c=a.y||a.clientY||a.offsetY||0}catch(d){c=b=0}0!=b&&0!=c&&this.addEntropy([b,c],2,"mouse");C(this,0)},qa:function(a){a=
+a.touches[0]||a.changedTouches[0];this.addEntropy([a.pageX||a.clientX,a.pageY||a.clientY],1,"touch");C(this,0)},ma:function(){C(this,2)},ea:function(a){a=a.accelerationIncludingGravity.x||a.accelerationIncludingGravity.y||a.accelerationIncludingGravity.z;if(window.orientation){var b=window.orientation;"number"===typeof b&&this.addEntropy(b,1,"accelerometer")}a&&this.addEntropy(a,2,"accelerometer");C(this,0)}};
+function A(a,b){var c,d=sjcl.random.K[a],e=[];for(c in d)d.hasOwnProperty(c)&&e.push(d[c]);for(c=0;c<e.length;c++)e[c](b)}function C(a,b){"undefined"!==typeof window&&window.performance&&"function"===typeof window.performance.now?a.addEntropy(window.performance.now(),b,"loadtime"):a.addEntropy((new Date).valueOf(),b,"loadtime")}function y(a){a.b=z(a).concat(z(a));a.L=new sjcl.cipher.aes(a.b)}function z(a){for(var b=0;4>b&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)}
+function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6);
+a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=require("crypto")}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F);
+else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))}
+sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length||
+4<f.iv.length)throw new sjcl.exception.invalid("json encrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,f),a=g.key.slice(0,f.ks/32),f.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.publicKey&&(g=a.kem(),f.kemtag=g.tag,a=g.key.slice(0,f.ks/32));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));"string"===typeof c&&(f.adata=c=sjcl.codec.utf8String.toBits(c));g=new sjcl.cipher[f.cipher](a);e.g(d,f);d.key=a;f.ct="ccm"===f.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&
+b instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.encrypt(g,b,f.iv,c,f.ts):sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return f},encrypt:function(a,b,c,d){var e=sjcl.json,f=e.ja.apply(e,arguments);return e.encode(f)},ia:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.g(e.g(e.g({},e.defaults),b),c,!0);var f,g;f=b.adata;"string"===typeof b.salt&&(b.salt=sjcl.codec.base64.toBits(b.salt));"string"===typeof b.iv&&(b.iv=sjcl.codec.base64.toBits(b.iv));if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||"string"===
+typeof a&&100>=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4<b.iv.length)throw new sjcl.exception.invalid("json decrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,b),a=g.key.slice(0,b.ks/32),b.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.secretKey&&(a=a.unkem(sjcl.codec.base64.toBits(b.kemtag)).slice(0,b.ks/32));"string"===typeof f&&(f=sjcl.codec.utf8String.toBits(f));g=new sjcl.cipher[b.cipher](a);f="ccm"===
+b.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&b.ct instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.decrypt(g,b.ct,b.iv,b.tag,f,b.ts):sjcl.mode[b.mode].decrypt(g,b.ct,b.iv,f,b.ts);e.g(d,b);d.key=a;return 1===c.raw?f:sjcl.codec.utf8String.fromBits(f)},decrypt:function(a,b,c,d){var e=sjcl.json;return e.ia(a,e.decode(b),c,d)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+
+b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],0)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^\s*(?:(["']?)([a-z][a-z0-9]*)\1)\s*:\s*(?:(-?\d+)|"([a-z0-9+\/%*_.@=\-]*)"|(true|false))$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");
+null!=d[3]?b[d[2]]=parseInt(d[3],10):null!=d[4]?b[d[2]]=d[2].match(/^(ct|adata|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4]):null!=d[5]&&(b[d[2]]="true"===d[5])}return b},g:function(a,b,c){void 0===a&&(a={});if(void 0===b)return a;for(var d in b)if(b.hasOwnProperty(d)){if(c&&void 0!==a[d]&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},sa:function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&a[d]!==b[d]&&(c[d]=a[d]);return c},ra:function(a,
+b){var c={},d;for(d=0;d<b.length;d++)void 0!==a[b[d]]&&(c[b[d]]=a[b[d]]);return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.pa={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.pa,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=void 0===b.salt?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
+"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){return sjcl});

+ 174 - 0
js/test.js

@@ -0,0 +1,174 @@
+'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'];
+
+global.$ = global.jQuery = require('./jquery-3.1.1');
+global.sjcl = require('./sjcl-1.0.6');
+global.Base64 = require('./base64-2.1.9');
+global.RawDeflate = require('./rawdeflate-0.5');
+require('./rawinflate-0.3');
+require('./privatebin');
+
+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';
+        });
+    });
+
+    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 && !(/&amp;/.test(result)));
+            }
+        );
+    });
+});
+
+describe('Model', function () {
+    describe('getPasteId', function () {
+        before(function () {
+            $.PrivateBin.Model.reset();
+        });
+
+        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;
+            }
+        );
+    });
+
+    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;
+            }
+        );
+    });
+});

+ 1 - 2
lib/.htaccess

@@ -1,2 +1 @@
-Allow from none
-Deny from all
+Require all denied

+ 32 - 32
lib/Configuration.php

@@ -7,12 +7,11 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
 
-use PrivateBin\I18n;
 use Exception;
 use PDO;
 
@@ -37,51 +36,52 @@ class Configuration
      */
     private static $_defaults = array(
         'main' => array(
-            'discussion' => true,
-            'opendiscussion' => false,
-            'password' => true,
-            'fileupload' => false,
+            'name'                     => 'PrivateBin',
+            'discussion'               => true,
+            'opendiscussion'           => false,
+            'password'                 => true,
+            'fileupload'               => false,
             'burnafterreadingselected' => false,
-            'defaultformatter' => 'plaintext',
-            'syntaxhighlightingtheme' => null,
-            'sizelimit' => 2097152,
-            'template' => 'bootstrap',
-            'notice' => '',
-            'languageselection' => false,
-            'languagedefault' => '',
-            'urlshortener' => '',
-            '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;',
-            'zerobincompatibility' => false,
+            'defaultformatter'         => 'plaintext',
+            'syntaxhighlightingtheme'  => null,
+            'sizelimit'                => 2097152,
+            'template'                 => 'bootstrap',
+            'notice'                   => '',
+            'languageselection'        => false,
+            'languagedefault'          => '',
+            'urlshortener'             => '',
+            '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',
+            'zerobincompatibility'     => false,
         ),
         'expire' => array(
             'default' => '1week',
-            'clone' => true,
+            'clone'   => true,
         ),
         'expire_options' => array(
-            '5min' => 300,
-            '10min' => 600,
-            '1hour' => 3600,
-            '1day' => 86400,
-            '1week' => 604800,
+            '5min'   => 300,
+            '10min'  => 600,
+            '1hour'  => 3600,
+            '1day'   => 86400,
+            '1week'  => 604800,
             '1month' => 2592000,
-            '1year' => 31536000,
-            'never' => 0,
+            '1year'  => 31536000,
+            'never'  => 0,
         ),
         'formatter_options' => array(
-            'plaintext' => 'Plain Text',
+            'plaintext'          => 'Plain Text',
             'syntaxhighlighting' => 'Source Code',
-            'markdown' => 'Markdown',
+            'markdown'           => 'Markdown',
         ),
         'traffic' => array(
-            'limit' => 10,
+            'limit'  => 10,
             'header' => null,
-            'dir' => 'data',
+            'dir'    => 'data',
         ),
         'purge' => array(
-            'limit' => 300,
+            'limit'     => 300,
             'batchsize' => 10,
-            'dir' => 'data',
+            'dir'       => 'data',
         ),
         'model' => array(
             'class' => 'Filesystem',
@@ -240,7 +240,7 @@ class Configuration
     public function getSection($section)
     {
         if (!array_key_exists($section, $this->_configuration)) {
-            throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 3);
+            throw new Exception(I18n::_('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3);
         }
         return $this->_configuration[$section];
     }

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Data;

+ 5 - 4
lib/Data/Database.php

@@ -7,15 +7,15 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Data;
 
-use PrivateBin\PrivateBin;
 use Exception;
 use PDO;
 use PDOException;
+use PrivateBin\PrivateBin;
 use stdClass;
 
 /**
@@ -417,7 +417,7 @@ class Database extends AbstractData
         $pastes = array();
         $rows   = self::_select(
             'SELECT dataid FROM ' . self::_sanitizeIdentifier('paste') .
-            ' WHERE expiredate < ? LIMIT ?', array(time(), $batchsize)
+            ' WHERE expiredate < ? AND expiredate != ? LIMIT ?', array(time(), 0, $batchsize)
         );
         if (count($rows)) {
             foreach ($rows as $row) {
@@ -701,10 +701,11 @@ class Database extends AbstractData
                 );
                 // no break, continue with updates for 0.22
             case '0.22':
+            case '1.0':
                 self::_exec(
                     'UPDATE ' . self::_sanitizeIdentifier('config') .
                     ' SET value = ? WHERE id = ?',
-                    array('1.0', 'VERSION')
+                    array(PrivateBin::VERSION, 'VERSION')
                 );
         }
     }

+ 2 - 2
lib/Data/Filesystem.php

@@ -7,13 +7,13 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Data;
 
-use PrivateBin\Model\Paste;
 use PrivateBin\Json;
+use PrivateBin\Model\Paste;
 
 /**
  * Filesystem

+ 1 - 17
lib/Filter.php

@@ -7,12 +7,11 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
 
-use PrivateBin\I18n;
 use Exception;
 
 /**
@@ -22,21 +21,6 @@ use Exception;
  */
 class Filter
 {
-    /**
-     * strips slashes deeply
-     *
-     * @access public
-     * @static
-     * @param  mixed $value
-     * @return mixed
-     */
-    public static function stripslashesDeep($value)
-    {
-        return is_array($value) ?
-            array_map('self::stripslashesDeep', $value) :
-            stripslashes($value);
-    }
-
     /**
      * format a given time string into a human readable label (localized)
      *

+ 10 - 5
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
@@ -295,13 +295,18 @@ class I18n
     {
         switch (self::$_language) {
             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);
-            // en, de
+                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);
+            case 'sl':
+                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;
         }
     }
 

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;

+ 1 - 2
lib/Model.php

@@ -7,12 +7,11 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
 
-use PrivateBin\Data;
 use PrivateBin\Model\Paste;
 use PrivateBin\Persistence\PurgeLimiter;
 

+ 2 - 2
lib/Model/AbstractModel.php

@@ -7,15 +7,15 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Model;
 
+use Exception;
 use PrivateBin\Configuration;
 use PrivateBin\Data\AbstractData;
 use PrivateBin\Sjcl;
-use Exception;
 use stdClass;
 
 /**

+ 4 - 4
lib/Model/Comment.php

@@ -7,16 +7,16 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Model;
 
-use PrivateBin\Sjcl;
+use Exception;
+use Identicon\Identicon;
 use PrivateBin\Persistence\TrafficLimiter;
+use PrivateBin\Sjcl;
 use PrivateBin\Vizhash16x16;
-use Identicon\Identicon;
-use Exception;
 
 /**
  * Comment

+ 3 - 4
lib/Model/Paste.php

@@ -7,15 +7,15 @@
  * @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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Model;
 
-use PrivateBin\PrivateBin;
+use Exception;
 use PrivateBin\Persistence\ServerSalt;
+use PrivateBin\PrivateBin;
 use PrivateBin\Sjcl;
-use Exception;
 
 /**
  * Paste
@@ -307,7 +307,6 @@ class Paste extends AbstractModel
                $this->_data->meta->burnafterreading === true;
     }
 
-
     /**
      * Check if paste has discussions enabled.
      *

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Persistence;

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Persistence;

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Persistence;

+ 1 - 1
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin\Persistence;

+ 24 - 19
lib/PrivateBin.php

@@ -7,14 +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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
 
-use PrivateBin\Persistence\TrafficLimiter;
-use PrivateBin\Persistence\ServerSalt;
 use Exception;
+use PrivateBin\Persistence\ServerSalt;
+use PrivateBin\Persistence\TrafficLimiter;
 
 /**
  * PrivateBin
@@ -28,7 +28,7 @@ class PrivateBin
      *
      * @const string
      */
-    const VERSION = '1.0';
+    const VERSION = '1.1';
 
     /**
      * show the same error message if the paste expired or does not exist
@@ -120,11 +120,11 @@ class PrivateBin
      */
     public function __construct()
     {
-        if (version_compare(PHP_VERSION, '5.3.0') < 0) {
-            throw new Exception(I18n::_('PrivateBin requires php 5.3.0 or above to work. Sorry.'), 1);
+        if (version_compare(PHP_VERSION, '5.4.0') < 0) {
+            throw new Exception(I18n::_('%s requires php 5.4.0 or above to work. Sorry.', I18n::_('PrivateBin')), 1);
         }
         if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
-            throw new Exception(I18n::_('PrivateBin requires the PATH to end in a "%s". Please update the PATH in your index.php.', DIRECTORY_SEPARATOR), 5);
+            throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5);
         }
 
         // load config from ini file, initialize required classes
@@ -334,19 +334,16 @@ class PrivateBin
                 // accessing this property ensures that the paste would be
                 // deleted if it has already expired
                 $burnafterreading = $paste->isBurnafterreading();
-                if ($deletetoken == 'burnafterreading') {
-                    if ($burnafterreading) {
-                        $paste->delete();
-                        $this->_return_message(0, $dataid);
-                    } else {
-                        $this->_return_message(1, 'Paste is not of burn-after-reading type.');
-                    }
+                if (
+                    ($burnafterreading && $deletetoken == 'burnafterreading') ||
+                    Filter::slowEquals($deletetoken, $paste->getDeleteToken())
+                ) {
+                    // Paste exists and deletion token is valid: Delete the paste.
+                    $paste->delete();
+                    $this->_status = 'Paste was properly deleted.';
                 } else {
-                    // Make sure the token is valid.
-                    if (Filter::slowEquals($deletetoken, $paste->getDeleteToken())) {
-                        // Paste exists and deletion token is valid: Delete the paste.
-                        $paste->delete();
-                        $this->_status = 'Paste was properly deleted.';
+                    if (!$burnafterreading && $deletetoken == 'burnafterreading') {
+                        $this->_error = 'Paste is not of burn-after-reading type.';
                     } else {
                         $this->_error = 'Wrong deletion token. Paste was not deleted.';
                     }
@@ -357,6 +354,13 @@ class PrivateBin
         } catch (Exception $e) {
             $this->_error = $e->getMessage();
         }
+        if ($this->_request->isJsonApiCall()) {
+            if (strlen($this->_error)) {
+                $this->_return_message(1, $this->_error);
+            } else {
+                $this->_return_message(0, $dataid);
+            }
+        }
     }
 
     /**
@@ -430,6 +434,7 @@ 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));

+ 1 - 8
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
@@ -80,13 +80,6 @@ class Request
      */
     public function __construct()
     {
-        // in case stupid admin has left magic_quotes enabled in php.ini (for PHP < 5.4)
-        if (version_compare(PHP_VERSION, '5.4.0') < 0 && get_magic_quotes_gpc()) {
-            $_POST   = array_map('PrivateBin\\Filter::stripslashesDeep', $_POST);
-            $_GET    = array_map('PrivateBin\\Filter::stripslashesDeep', $_GET);
-            $_COOKIE = array_map('PrivateBin\\Filter::stripslashesDeep', $_COOKIE);
-        }
-
         // decide if we are in JSON API or HTML context
         $this->_isJsonApi = $this->_detectJsonRequest();
 

+ 2 - 2
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
@@ -31,7 +31,7 @@ class Sjcl
      */
     public static function isValid($encoded)
     {
-        $accepted_keys = array('iv','v','iter','ks','ts','mode','adata','cipher','salt','ct');
+        $accepted_keys = array('iv', 'v', 'iter', 'ks', 'ts', 'mode', 'adata', 'cipher', 'salt', 'ct');
 
         // Make sure content is valid json
         $decoded = json_decode($encoded);

+ 3 - 2
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.0
+ * @version   1.1
  */
 
 namespace PrivateBin;
@@ -52,7 +52,8 @@ class View
      */
     public function draw($template)
     {
-        $path = PATH . 'tpl' . DIRECTORY_SEPARATOR . $template . '.php';
+        $file = substr($template, 0, 9) === 'bootstrap' ? 'bootstrap' : $template;
+        $path = PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php';
         if (!file_exists($path)) {
             throw new Exception('Template ' . $template . ' not found!', 80);
         }

+ 7 - 8
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.0
+ * @version   0.0.5 beta PrivateBin 1.1
  */
 
 namespace PrivateBin;
@@ -23,7 +23,6 @@ namespace PrivateBin;
  * echo $data;
  * exit;
  */
-
 class Vizhash16x16
 {
     /**
@@ -105,7 +104,7 @@ class Vizhash16x16
         $op = 'v';
         if (($this->getInt() % 2) == 0) {
             $op = 'h';
-        };
+        }
         $image = $this->degrade($image, $op, array($r0, $g0, $b0), array(0, 0, 0));
 
         for ($i = 0; $i < 7; ++$i) {
@@ -189,7 +188,7 @@ class Vizhash16x16
         $diffs = array(
             (($color2[0] - $color1[0]) / $size),
             (($color2[1] - $color1[1]) / $size),
-            (($color2[2] - $color1[2]) / $size)
+            (($color2[2] - $color1[2]) / $size),
         );
         for ($i = 0; $i < $size; ++$i) {
             $r = $color1[0] + ($diffs[0] * $i);
@@ -217,20 +216,20 @@ class Vizhash16x16
     {
         switch ($action % 7) {
             case 0:
-                ImageFilledRectangle($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
+                imagefilledrectangle($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
                 break;
             case 1:
             case 2:
-                ImageFilledEllipse($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
+                imagefilledellipse($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
                 break;
             case 3:
                 $points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY());
-                ImageFilledPolygon($image, $points, 4, $color);
+                imagefilledpolygon($image, $points, 4, $color);
                 break;
             default:
                 $start = $this->getInt() * 360 / 256;
                 $end   = $start + $this->getInt() * 180 / 256;
-                ImageFilledArc($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $start, $end, $color, IMG_ARC_PIE);
+                imagefilledarc($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $start, $end, $color, IMG_ARC_PIE);
         }
     }
 }

+ 2 - 2
manifest.json

@@ -2,12 +2,12 @@
 	"name": "PrivateBin",
 	"icons": [
 		{
-			"src": "android-chrome-192x192.png",
+			"src": "img/android-chrome-192x192.png",
 			"sizes": "192x192",
 			"type": "image\/png"
 		},
 		{
-			"src": "android-chrome-512x512.png",
+			"src": "img/android-chrome-512x512.png",
 			"sizes": "512x512",
 			"type": "image\/png"
 		}

+ 9 - 0
tpl/.editorconfig

@@ -0,0 +1,9 @@
+# editorconfig.org
+
+root = false
+
+# special format for PHP templates
+[*.php]
+indent_style = tab
+indent_size = 4
+

+ 1 - 0
tpl/.htaccess

@@ -0,0 +1 @@
+Require all denied

+ 0 - 343
tpl/bootstrap-compact.php

@@ -1,343 +0,0 @@
-<?php
-use PrivateBin\I18n;
-?><!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<meta http-equiv="X-UA-Compatible" content="IE=edge">
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<meta name="robots" content="noindex" />
-		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    if (strlen($SYNTAXHIGHLIGHTINGTHEME)):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/<?php echo rawurlencode($SYNTAXHIGHLIGHTINGTHEME); ?>.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    endif;
-endif;
-?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
-<?php
-if ($ZEROBINCOMPATIBILITY):
-?>
-		<script type="text/javascript" src="js/base64-1.7.js" integrity="sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==" crossorigin="anonymous"></script>
-<?php
-else:
-?>
-		<script type="text/javascript" src="js/base64-2.1.9.js" integrity="sha512-rbqAby7hObftbEoGQzkhUbEh5YkUn2MtekTLs4btvo2oly4CZ3DxhJzEh0u/rNzS54tcJdqi5Ug1ruugEd2U1g==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/rawdeflate-0.5.js" integrity="sha512-tTdZ7qMr7tt5VQy4iCHu6/aGB12eRwbUy+AEI5rXntfsjcRfBeeqJloMsBU9FrGk1bIYLiuND/FhU42LO1bi0g==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/rawinflate-0.3.js" integrity="sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/bootstrap-3.3.5.js" integrity="sha512-/W33QnLmSAP1fwINS9iXgB6s/VOIG9GVdIuIYaUtbSvKPMv5S08PtT3PqnT2WjwBgB8DFeDN2nqJroqQYF7SwQ==" crossorigin="anonymous"></script>
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<script type="text/javascript" src="js/prettify.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-m8iHxoN+Fe12xxFwWNdY/TS4KoFntHp29qY0xUzBnPd0bkKMOR/dFhEdTWydpt0b/fIXyhB+znGYUvgjfJ2RzQ==" crossorigin="anonymous"></script>
-<?php
-endif;
-if ($MARKDOWN):
-?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
-		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
-		<meta name="msapplication-config" content="browserconfig.xml">
-		<meta name="theme-color" content="#ffe57e" />
-	</head>
-	<body role="document" class="navbar-spacing">
-		<nav class="navbar navbar-default navbar-fixed-top">
-			<div class="container">
-				<div class="navbar-header">
-					<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-						<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-					</button>
-					<a class="reloadlink navbar-brand" href="">
-						<img alt="<?php echo I18n::_('PrivateBin'); ?>" src="img/icon.svg" width="38" />
-					</a>
-				</div>
-				<div id="navbar" class="navbar-collapse collapse">
-					<ul class="nav navbar-nav">
-						<li>
-							<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn">
-								<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL; ?>
-							</button>
-<?php
-if ($EXPIRECLONE):
-?>
-							<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
-								<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
-							</button>
-<?php
-endif;
-?>
-							<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
-								<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
-							</button>
-						</li>
-						<li class="dropdown">
-							<select id="pasteExpiration" name="pasteExpiration" class="hidden">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-								<option value="<?php echo $key; ?>"<?php
-    if ($key == $EXPIREDEFAULT):
-?> selected="selected"<?php
-    endif;
-?>><?php echo $value; ?></option>
-<?php
-endforeach;
-?>
-							</select>
-							<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Expires'); ?>: <span id="pasteExpirationDisplay"><?php echo $EXPIRE[$EXPIREDEFAULT]; ?></span> <span class="caret"></span></a>
-							<ul class="dropdown-menu">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-								<li>
-									<a href="#" data-expiration="<?php echo $key; ?>">
-										<?php echo $value, PHP_EOL; ?>
-									</a>
-								</li>
-<?php
-endforeach;
-?>
-							</ul>
-						</li>
-						<li id="formatter" class="dropdown">
-							<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Options'); ?> <span class="caret"></span></a>
-							<ul class="dropdown-menu">
-								<li id="burnafterreadingoption" class="checkbox hidden">
-									<label>
-										<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-if ($BURNAFTERREADINGSELECTED):
-?> checked="checked"<?php
-endif;
-?> />
-										<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-									</label>
-								</li>
-<?php
-if ($DISCUSSION):
-?>
-								<li id="opendisc" class="checkbox hidden">
-									<label>
-										<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-    if ($OPENDISCUSSION):
-?> checked="checked"<?php
-    endif;
-?> />
-										<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-									</label>
-								</li>
-<?php
-endif;
-?>
-								<li role="separator" class="divider"></li>
-								<li>
-									<div>
-										<?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span>
-									</div>
-								</li>
-<?php
-foreach ($FORMATTER as $key => $value):
-?>
-								<li>
-									<a href="#" data-format="<?php echo $key; ?>">
-										<?php echo $value, PHP_EOL; ?>
-									</a>
-								</li>
-<?php
-endforeach;
-?>
-							</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
-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"/>
-							</div>
-						</li>
-<?php
-endif;
-if ($FILEUPLOAD):
-?>
-						<li id="attach" class="hidden dropdown">
-							<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('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>
-									<a id="fileremovebutton"  href="#">
-										<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
-									</a>
-								</li>
-							</ul>
-						</li>
-<?php
-endif;
-?>
-					</ul>
-					<ul class="nav navbar-nav pull-right">
-<?php
-if (strlen($LANGUAGESELECTION)):
-?>
-						<li id="language" class="dropdown">
-							<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> <?php echo $LANGUAGES[$LANGUAGESELECTION][0]; ?> <span class="caret"></span></a>
-							<ul class="dropdown-menu">
-<?php
-    foreach ($LANGUAGES as $key => $value):
-?>
-								<li>
-									<a href="#" data-lang="<?php echo $key; ?>">
-										<?php echo $value[0]; ?> (<?php echo $value[1]; ?>)
-									</a>
-								</li>
-<?php
-    endforeach;
-?>
-							</ul>
-						</li>
-<?php
-endif;
-?>
-						<li>
-							<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
-								<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL; ?>
-							</button>
-						</li>
-					</ul>
-				</div>
-			</div>
-		</nav>
-		<header class="container">
-<?php
-if (strlen($NOTICE)):
-?>
-			<div role="alert" class="alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="remainingtime" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-			</div>
-<?php
-if ($FILEUPLOAD):
-?>
-			<div id="attachment" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a><?php echo I18n::_('Download attachment'); ?></a> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-			</div>
-<?php
-endif;
-if (strlen($STATUS)):
-?>
-			<div id="status" role="alert" class="alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="errormessage" role="alert" class="<?php
-if (!strlen($ERROR)):
-?>hidden <?php
-endif;
-?>alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo htmlspecialchars($ERROR); ?></div>
-			<noscript><div id="noscript" role="alert" class="nonworking alert alert-warning"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
-			<div id="ienotice" role="alert" class="hidden alert alert-warning"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> <?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
-				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
-			</div>
-			<div id="pasteresult" role="alert" class="hidden alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
-				<div id="deletelink"></div>
-				<div id="pastelink">
-<?php
-if (strlen($URLSHORTENER)):
-?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-primary">
-						<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
-					</button>
-<?php
-endif;
-?>
-				</div>
-			</div>
-			<ul id="preview" class="nav nav-tabs hidden">
-				<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-				<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-			</ul>
-		</header>
-		<section class="container">
-			<article class="row">
-				<div id="image" class="col-md-12 text-center hidden"></div>
-				<div id="prettymessage" class="col-md-12 hidden">
-					<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
-				</div>
-				<div id="cleartext" class="col-md-12 hidden"></div>
-				<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
-			</article>
-		</section>
-		<section class="container">
-			<div id="discussion" class="hidden">
-				<h4><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
-			</div>
-		</section>
-		<footer class="container">
-			<div class="row">
-				<h4 class="col-md-5 col-xs-8"><?php echo I18n::_('PrivateBin'); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-				<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-				<p id="aboutbox" class="col-md-6 col-xs-12">
-					<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'), PHP_EOL; ?>
-				</p>
-			</div>
-		</footer>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
-	</body>
-</html>

+ 0 - 337
tpl/bootstrap-dark-page.php

@@ -1,337 +0,0 @@
-<?php
-use PrivateBin\I18n;
-?><!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<meta http-equiv="X-UA-Compatible" content="IE=edge">
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<meta name="robots" content="noindex" />
-		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    if (strlen($SYNTAXHIGHLIGHTINGTHEME)):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/<?php echo rawurlencode($SYNTAXHIGHLIGHTINGTHEME); ?>.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    endif;
-endif;
-?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
-<?php
-if ($ZEROBINCOMPATIBILITY):
-?>
-		<script type="text/javascript" src="js/base64-1.7.js" integrity="sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==" crossorigin="anonymous"></script>
-<?php
-else:
-?>
-		<script type="text/javascript" src="js/base64-2.1.9.js" integrity="sha512-rbqAby7hObftbEoGQzkhUbEh5YkUn2MtekTLs4btvo2oly4CZ3DxhJzEh0u/rNzS54tcJdqi5Ug1ruugEd2U1g==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/rawdeflate-0.5.js" integrity="sha512-tTdZ7qMr7tt5VQy4iCHu6/aGB12eRwbUy+AEI5rXntfsjcRfBeeqJloMsBU9FrGk1bIYLiuND/FhU42LO1bi0g==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/rawinflate-0.3.js" integrity="sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/bootstrap-3.3.5.js" integrity="sha512-/W33QnLmSAP1fwINS9iXgB6s/VOIG9GVdIuIYaUtbSvKPMv5S08PtT3PqnT2WjwBgB8DFeDN2nqJroqQYF7SwQ==" crossorigin="anonymous"></script>
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<script type="text/javascript" src="js/prettify.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-m8iHxoN+Fe12xxFwWNdY/TS4KoFntHp29qY0xUzBnPd0bkKMOR/dFhEdTWydpt0b/fIXyhB+znGYUvgjfJ2RzQ==" crossorigin="anonymous"></script>
-<?php
-endif;
-if ($MARKDOWN):
-?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
-		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
-		<meta name="msapplication-config" content="browserconfig.xml">
-		<meta name="theme-color" content="#ffe57e" />
-	</head>
-	<body role="document">
-		<nav class="navbar navbar-inverse navbar-static-top">
-			<div class="navbar-header">
-				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-					<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-				</button>
-				<a class="reloadlink navbar-brand" href="">
-					<img alt="<?php echo I18n::_('PrivateBin'); ?>" src="img/icon.svg" width="38" />
-				</a>
-			</div>
-			<div id="navbar" class="navbar-collapse collapse">
-				<ul class="nav navbar-nav">
-					<li>
-						<button id="sendbutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL; ?>
-						</button>
-<?php
-if ($EXPIRECLONE):
-?>
-						<button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
-						</button>
-<?php
-endif;
-?>
-						<button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
-						</button>
-					</li>
-					<li class="dropdown">
-						<select id="pasteExpiration" name="pasteExpiration" class="hidden">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<option value="<?php echo $key; ?>"<?php
-    if ($key == $EXPIREDEFAULT):
-?> selected="selected"<?php
-    endif;
-?>><?php echo $value; ?></option>
-<?php
-endforeach;
-?>
-						</select>
-						<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Expires'); ?>: <span id="pasteExpirationDisplay"><?php echo $EXPIRE[$EXPIREDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<li>
-								<a href="#" data-expiration="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-					<li>
-						<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-if ($BURNAFTERREADINGSELECTED):
-?> checked="checked"<?php
-endif;
-?> />
-								<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-							</label>
-						</div>
-					</li>
-<?php
-if ($DISCUSSION):
-?>
-					<li>
-						<div id="opendisc" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-    if ($OPENDISCUSSION):
-?> checked="checked"<?php
-    endif;
-?> />
-								<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-						 	</label>
-						</div>
-					</li>
-<?php
-endif;
-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" />
-						</div>
-					</li>
-<?php
-endif;
-if ($FILEUPLOAD):
-?>
-					<li id="attach" class="hidden dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('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>
-								<a id="fileremovebutton"  href="#">
-									<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
-								</a>
-							</li>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li class="dropdown">
-						<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>
-						<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($FORMATTER as $key => $value):
-?>
-							<li>
-								<a href="#" data-format="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-				</ul>
-				<ul class="nav navbar-nav pull-right">
-<?php
-if (strlen($LANGUAGESELECTION)):
-?>
-					<li id="language" class="dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> <?php echo $LANGUAGES[$LANGUAGESELECTION][0]; ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-    foreach ($LANGUAGES as $key => $value):
-?>
-							<li>
-								<a href="#" data-lang="<?php echo $key; ?>">
-									<?php echo $value[0]; ?> (<?php echo $value[1]; ?>)
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL; ?>
-						</button>
-					</li>
-				</ul>
-			</div>
-		</nav>
-		<header class="container">
-<?php
-if (strlen($NOTICE)):
-?>
-			<div role="alert" class="alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="remainingtime" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-			</div>
-<?php
-if ($FILEUPLOAD):
-?>
-			<div id="attachment" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a><?php echo I18n::_('Download attachment'); ?></a> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-			</div>
-<?php
-endif;
-if (strlen($STATUS)):
-?>
-			<div id="status" role="alert" class="alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="errormessage" role="alert" class="<?php
-if (!strlen($ERROR)):
-?>hidden <?php
-endif;
-?>alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo htmlspecialchars($ERROR); ?></div>
-			<noscript><div id="noscript" role="alert" class="nonworking alert alert-error"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
-			<div id="ienotice" role="alert" class="hidden alert alert-error"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> <?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
-				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
-			</div>
-			<div id="pasteresult" role="alert" class="hidden alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
-				<div id="deletelink"></div>
-				<div id="pastelink">
-<?php
-if (strlen($URLSHORTENER)):
-?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-warning">
-						<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
-					</button>
-<?php
-endif;
-?>
-				</div>
-			</div>
-			<ul id="preview" class="nav nav-tabs hidden">
-				<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-				<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-			</ul>
-		</header>
-		<section class="container">
-			<article class="row">
-				<div id="image" class="col-md-12 text-center hidden"></div>
-				<div id="prettymessage" class="col-md-12 hidden">
-					<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
-				</div>
-				<div id="cleartext" class="col-md-12 hidden"></div>
-				<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
-			</article>
-		</section>
-		<section class="container">
-			<div id="discussion" class="hidden">
-				<h4><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
-			</div>
-		</section>
-		<footer class="container">
-			<div class="row">
-				<h4 class="col-md-5 col-xs-8"><?php echo I18n::_('PrivateBin'); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-				<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-				<p id="aboutbox" class="col-md-6 col-xs-12">
-					<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'), PHP_EOL; ?>
-				</p>
-			</div>
-		</footer>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
-	</body>
-</html>

+ 0 - 337
tpl/bootstrap-dark.php

@@ -1,337 +0,0 @@
-<?php
-use PrivateBin\I18n;
-?><!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<meta http-equiv="X-UA-Compatible" content="IE=edge">
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<meta name="robots" content="noindex" />
-		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-     if (strlen($SYNTAXHIGHLIGHTINGTHEME)):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/<?php echo rawurlencode($SYNTAXHIGHLIGHTINGTHEME); ?>.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    endif;
-endif;
-?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
-<?php
-if ($ZEROBINCOMPATIBILITY):
-?>
-		<script type="text/javascript" src="js/base64-1.7.js" integrity="sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==" crossorigin="anonymous"></script>
-<?php
-else:
-?>
-		<script type="text/javascript" src="js/base64-2.1.9.js" integrity="sha512-rbqAby7hObftbEoGQzkhUbEh5YkUn2MtekTLs4btvo2oly4CZ3DxhJzEh0u/rNzS54tcJdqi5Ug1ruugEd2U1g==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/rawdeflate-0.5.js" integrity="sha512-tTdZ7qMr7tt5VQy4iCHu6/aGB12eRwbUy+AEI5rXntfsjcRfBeeqJloMsBU9FrGk1bIYLiuND/FhU42LO1bi0g==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/rawinflate-0.3.js" integrity="sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/bootstrap-3.3.5.js" integrity="sha512-/W33QnLmSAP1fwINS9iXgB6s/VOIG9GVdIuIYaUtbSvKPMv5S08PtT3PqnT2WjwBgB8DFeDN2nqJroqQYF7SwQ==" crossorigin="anonymous"></script>
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<script type="text/javascript" src="js/prettify.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-m8iHxoN+Fe12xxFwWNdY/TS4KoFntHp29qY0xUzBnPd0bkKMOR/dFhEdTWydpt0b/fIXyhB+znGYUvgjfJ2RzQ==" crossorigin="anonymous"></script>
-<?php
-endif;
-if ($MARKDOWN):
-?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
-		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
-		<meta name="msapplication-config" content="browserconfig.xml">
-		<meta name="theme-color" content="#ffe57e" />
-	</head>
-	<body role="document">
-		<nav class="navbar navbar-inverse navbar-static-top">
-			<div class="navbar-header">
-				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-					<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-				</button>
-				<a class="reloadlink navbar-brand" href="">
-					<img alt="<?php echo I18n::_('PrivateBin'); ?>" src="img/icon.svg" width="38" />
-				</a>
-			</div>
-			<div id="navbar" class="navbar-collapse collapse">
-				<ul class="nav navbar-nav">
-					<li>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL; ?>
-						</button>
-<?php
-if ($EXPIRECLONE):
-?>
-						<button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
-						</button>
-<?php
-endif;
-?>
-						<button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
-						</button>
-					</li>
-					<li class="dropdown">
-						<select id="pasteExpiration" name="pasteExpiration" class="hidden">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<option value="<?php echo $key; ?>"<?php
-    if ($key == $EXPIREDEFAULT):
-?> selected="selected"<?php
-    endif;
-?>><?php echo $value; ?></option>
-<?php
-endforeach;
-?>
-						</select>
-						<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Expires'); ?>: <span id="pasteExpirationDisplay"><?php echo $EXPIRE[$EXPIREDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<li>
-								<a href="#" data-expiration="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-					<li>
-						<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-if ($BURNAFTERREADINGSELECTED):
-?> checked<?php
-endif;
-?> />
-								<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-							</label>
-						</div>
-					</li>
-<?php
-if ($DISCUSSION):
-?>
-					<li>
-						<div id="opendisc" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-    if ($OPENDISCUSSION):
-?> checked<?php
-    endif;
-?> />
-								<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-						 	</label>
-						</div>
-					</li>
-<?php
-endif;
-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" />
-						</div>
-					</li>
-<?php
-endif;
-if ($FILEUPLOAD):
-?>
-					<li id="attach" class="hidden dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('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>
-								<a id="fileremovebutton"  href="#">
-									<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
-								</a>
-							</li>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li class="dropdown">
-						<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>
-						<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($FORMATTER as $key => $value):
-?>
-							<li>
-								<a href="#" data-format="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-				</ul>
-				<ul class="nav navbar-nav pull-right">
-<?php
-if (strlen($LANGUAGESELECTION)):
-?>
-					<li id="language" class="dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> <?php echo $LANGUAGES[$LANGUAGESELECTION][0]; ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-    foreach ($LANGUAGES as $key => $value):
-?>
-							<li>
-								<a href="#" data-lang="<?php echo $key; ?>">
-									<?php echo $value[0]; ?> (<?php echo $value[1]; ?>)
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li>
-						<button id="sendbutton" type="button" class="hidden btn btn-warning navbar-btn">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL; ?>
-						</button>
-					</li>
-				</ul>
-			</div>
-		</nav>
-		<header class="container">
-<?php
-if (strlen($NOTICE)):
-?>
-			<div role="alert" class="alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="remainingtime" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-			</div>
-<?php
-if ($FILEUPLOAD):
-?>
-			<div id="attachment" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a><?php echo I18n::_('Download attachment'); ?></a> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-			</div>
-<?php
-endif;
-if (strlen($STATUS)):
-?>
-			<div id="status" role="alert" class="alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="errormessage" role="alert" class="<?php
-if (!strlen($ERROR)):
-?>hidden <?php
-endif;
-?>alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo htmlspecialchars($ERROR); ?></div>
-			<noscript><div id="noscript" role="alert" class="nonworking alert alert-error"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
-			<div id="ienotice" role="alert" class="hidden alert alert-error"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> <?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
-				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
-			</div>
-			<div id="pasteresult" role="alert" class="hidden alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
-				<div id="deletelink"></div>
-				<div id="pastelink">
-<?php
-if (strlen($URLSHORTENER)):
-?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-warning">
-						<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
-					</button>
-<?php
-endif;
-?>
-				</div>
-			</div>
-			<ul id="preview" class="nav nav-tabs hidden">
-				<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-				<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-			</ul>
-		</header>
-		<section class="container">
-			<article class="row">
-				<div id="image" class="col-md-12 text-center hidden"></div>
-				<div id="prettymessage" class="col-md-12 hidden">
-					<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
-				</div>
-				<div id="cleartext" class="col-md-12 hidden"></div>
-				<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
-			</article>
-		</section>
-		<section class="container">
-			<div id="discussion" class="hidden">
-				<h4><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
-			</div>
-		</section>
-		<footer class="container">
-			<div class="row">
-				<h4 class="col-md-5 col-xs-8"><?php echo I18n::_('PrivateBin'); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-				<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-				<p id="aboutbox" class="col-md-6 col-xs-12">
-					<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'), PHP_EOL; ?>
-				</p>
-			</div>
-		</footer>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
-	</body>
-</html>

+ 0 - 337
tpl/bootstrap-page.php

@@ -1,337 +0,0 @@
-<?php
-use PrivateBin\I18n;
-?><!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<meta http-equiv="X-UA-Compatible" content="IE=edge">
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<meta name="robots" content="noindex" />
-		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    if (strlen($SYNTAXHIGHLIGHTINGTHEME)):
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/<?php echo rawurlencode($SYNTAXHIGHLIGHTINGTHEME); ?>.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    endif;
-endif;
-?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
-<?php
-if ($ZEROBINCOMPATIBILITY):
-?>
-		<script type="text/javascript" src="js/base64-1.7.js" integrity="sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==" crossorigin="anonymous"></script>
-<?php
-else:
-?>
-		<script type="text/javascript" src="js/base64-2.1.9.js" integrity="sha512-rbqAby7hObftbEoGQzkhUbEh5YkUn2MtekTLs4btvo2oly4CZ3DxhJzEh0u/rNzS54tcJdqi5Ug1ruugEd2U1g==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/rawdeflate-0.5.js" integrity="sha512-tTdZ7qMr7tt5VQy4iCHu6/aGB12eRwbUy+AEI5rXntfsjcRfBeeqJloMsBU9FrGk1bIYLiuND/FhU42LO1bi0g==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/rawinflate-0.3.js" integrity="sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/bootstrap-3.3.5.js" integrity="sha512-/W33QnLmSAP1fwINS9iXgB6s/VOIG9GVdIuIYaUtbSvKPMv5S08PtT3PqnT2WjwBgB8DFeDN2nqJroqQYF7SwQ==" crossorigin="anonymous"></script>
-<?php
-if ($SYNTAXHIGHLIGHTING):
-?>
-		<script type="text/javascript" src="js/prettify.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-m8iHxoN+Fe12xxFwWNdY/TS4KoFntHp29qY0xUzBnPd0bkKMOR/dFhEdTWydpt0b/fIXyhB+znGYUvgjfJ2RzQ==" crossorigin="anonymous"></script>
-<?php
-endif;
-if ($MARKDOWN):
-?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
-<?php
-endif;
-?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
-		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
-		<meta name="msapplication-config" content="browserconfig.xml">
-		<meta name="theme-color" content="#ffe57e" />
-	</head>
-	<body role="document">
-		<nav class="navbar navbar-default navbar-static-top">
-			<div class="navbar-header">
-				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-					<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-				</button>
-				<a class="reloadlink navbar-brand" href="">
-					<img alt="<?php echo I18n::_('PrivateBin'); ?>" src="img/icon.svg" width="38" />
-				</a>
-			</div>
-			<div id="navbar" class="navbar-collapse collapse">
-				<ul class="nav navbar-nav">
-					<li>
-						<button id="sendbutton" type="button" class="hidden btn btn-primary navbar-btn">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL; ?>
-						</button>
-<?php
-if ($EXPIRECLONE):
-?>
-						<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
-							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
-						</button>
-<?php
-endif;
-?>
-						<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
-							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
-						</button>
-					</li>
-					<li class="dropdown">
-						<select id="pasteExpiration" name="pasteExpiration" class="hidden">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<option value="<?php echo $key; ?>"<?php
-    if ($key == $EXPIREDEFAULT):
-?> selected="selected"<?php
-    endif;
-?>><?php echo $value; ?></option>
-<?php
-endforeach;
-?>
-						</select>
-						<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Expires'); ?>: <span id="pasteExpirationDisplay"><?php echo $EXPIRE[$EXPIREDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($EXPIRE as $key => $value):
-?>
-							<li>
-								<a href="#" data-expiration="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-					<li>
-						<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-if ($BURNAFTERREADINGSELECTED):
-?> checked="checked"<?php
-endif;
-?> />
-								<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-							</label>
-						</div>
-					</li>
-<?php
-if ($DISCUSSION):
-?>
-					<li>
-						<div id="opendisc" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-    if ($OPENDISCUSSION):
-?> checked="checked"<?php
-    endif;
-?> />
-								<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-						 	</label>
-						</div>
-					</li>
-<?php
-endif;
-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" />
-						</div>
-					</li>
-<?php
-endif;
-if ($FILEUPLOAD):
-?>
-					<li id="attach" class="hidden dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('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>
-								<a id="fileremovebutton"  href="#">
-									<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
-								</a>
-							</li>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li class="dropdown">
-						<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>
-						<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($FORMATTER as $key => $value):
-?>
-							<li>
-								<a href="#" data-format="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-				</ul>
-				<ul class="nav navbar-nav pull-right">
-<?php
-if (strlen($LANGUAGESELECTION)):
-?>
-					<li id="language" class="dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> <?php echo $LANGUAGES[$LANGUAGESELECTION][0]; ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-    foreach ($LANGUAGES as $key => $value):
-?>
-							<li>
-								<a href="#" data-lang="<?php echo $key; ?>">
-									<?php echo $value[0]; ?> (<?php echo $value[1]; ?>)
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-					<li>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL; ?>
-						</button>
-					</li>
-				</ul>
-			</div>
-		</nav>
-		<header class="container">
-<?php
-if (strlen($NOTICE)):
-?>
-			<div role="alert" class="alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="remainingtime" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-			</div>
-<?php
-if ($FILEUPLOAD):
-?>
-			<div id="attachment" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a><?php echo I18n::_('Download attachment'); ?></a> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-			</div>
-<?php
-endif;
-if (strlen($STATUS)):
-?>
-			<div id="status" role="alert" class="alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
-			</div>
-<?php
-endif;
-?>
-			<div id="errormessage" role="alert" class="<?php
-if (!strlen($ERROR)):
-?>hidden <?php
-endif;
-?>alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo htmlspecialchars($ERROR); ?></div>
-			<noscript><div id="noscript" role="alert" class="nonworking alert alert-warning"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
-			<div id="ienotice" role="alert" class="hidden alert alert-warning"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> <?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
-				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
-			</div>
-			<div id="pasteresult" role="alert" class="hidden alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
-				<div id="deletelink"></div>
-				<div id="pastelink">
-<?php
-if (strlen($URLSHORTENER)):
-?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-primary">
-						<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
-					</button>
-<?php
-endif;
-?>
-				</div>
-			</div>
-			<ul id="preview" class="nav nav-tabs hidden">
-				<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-				<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-			</ul>
-		</header>
-		<section class="container">
-			<article class="row">
-				<div id="image" class="col-md-12 text-center hidden"></div>
-				<div id="prettymessage" class="col-md-12 hidden">
-					<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
-				</div>
-				<div id="cleartext" class="col-md-12 hidden"></div>
-				<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
-			</article>
-		</section>
-		<section class="container">
-			<div id="discussion" class="hidden">
-				<h4><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
-			</div>
-		</section>
-		<footer class="container">
-			<div class="row">
-				<h4 class="col-md-5 col-xs-8"><?php echo I18n::_('PrivateBin'); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-				<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-				<p id="aboutbox" class="col-md-6 col-xs-12">
-					<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'), PHP_EOL; ?>
-				</p>
-			</div>
-		</footer>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
-	</body>
-</html>

+ 260 - 101
tpl/bootstrap.php

@@ -1,16 +1,32 @@
 <?php
 use PrivateBin\I18n;
+$isCpct = substr($template, 9, 8) === '-compact';
+$isDark = substr($template, 9, 5) === '-dark';
+$isPage = substr($template, -5) === '-page';
 ?><!DOCTYPE html>
-<html lang="en">
+<html>
 	<head>
 		<meta charset="utf-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge">
 		<meta name="viewport" content="width=device-width, initial-scale=1">
 		<meta name="robots" content="noindex" />
 		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
+		<title><?php echo I18n::_($NAME); ?></title>
+<?php
+if (!$isDark):
+?>
 		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-3.3.5.css" />
+<?php
+endif;
+?>
 		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
+<?php
+if ($isDark):
+?>
+		<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
+<?php
+endif;
+?>
 		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
 <?php
 if ($SYNTAXHIGHLIGHTING):
@@ -24,8 +40,9 @@ if ($SYNTAXHIGHLIGHTING):
     endif;
 endif;
 ?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
+		<noscript><link type="text/css" rel="stylesheet" href="css/noscript.css" /></noscript>
+		<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 ($ZEROBINCOMPATIBILITY):
 ?>
@@ -39,7 +56,7 @@ endif;
 ?>
 		<script type="text/javascript" src="js/rawdeflate-0.5.js" integrity="sha512-tTdZ7qMr7tt5VQy4iCHu6/aGB12eRwbUy+AEI5rXntfsjcRfBeeqJloMsBU9FrGk1bIYLiuND/FhU42LO1bi0g==" crossorigin="anonymous"></script>
 		<script type="text/javascript" src="js/rawinflate-0.3.js" integrity="sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/bootstrap-3.3.5.js" integrity="sha512-/W33QnLmSAP1fwINS9iXgB6s/VOIG9GVdIuIYaUtbSvKPMv5S08PtT3PqnT2WjwBgB8DFeDN2nqJroqQYF7SwQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/bootstrap-3.3.7.js" integrity="sha512-iztkobsvnjKfAtTNdHkGVjAYTrrtlC7mGp/54c40wowO7LhURYl3gVzzcEqGl/qKXQltJ2HwMrdLcNUdo+N/RQ==" crossorigin="anonymous"></script>
 <?php
 if ($SYNTAXHIGHLIGHTING):
 ?>
@@ -48,25 +65,48 @@ if ($SYNTAXHIGHLIGHTING):
 endif;
 if ($MARKDOWN):
 ?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/showdown-1.6.1.js" integrity="sha512-e6kAsBTgFnTBnEQXrq8BV6+XFwxb3kyWHeEPOl+KhxaWt3xImE2zAW2+yP3E2CQ7F9yoJl1poVU9qxkOEtVsTQ==" crossorigin="anonymous"></script>
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7/bLmiTErcH3lMJi1JxXk/lb20NUO7TCqasWeFybnNhnfOUnJ9GQGqPVJxPf+7fnw9dgLa1s18FXQuRanLD8Hw==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
+		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
+		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
+		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
 		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
+		<link rel="mask-icon" href="img/safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
+		<link rel="shortcut icon" href="img/favicon.ico">
 		<meta name="msapplication-config" content="browserconfig.xml">
 		<meta name="theme-color" content="#ffe57e" />
 	</head>
-	<body role="document">
-		<nav class="navbar navbar-default navbar-static-top">
+	<body role="document"<?php
+if ($isCpct):
+?> class="navbar-spacing"<?php
+endif;
+?>>
+		<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">
+								<label for="passworddecrypt"><span class="glyphicon glyphicon-eye-open"></span> <?php echo I18n::_('Please enter the password for this paste:') ?></label>
+								<input id="passworddecrypt" type="password" class="form-control" placeholder="<?php echo I18n::_('Enter password') ?>">
+							</div>
+							<button type="submit" class="btn btn-success btn-block"><span class="glyphicon glyphicon-off"></span> <?php echo I18n::_('Decrypt') ?></button>
+						</form>
+					</div>
+				</div>
+			</div>
+		</div>
+		<nav class="navbar navbar-<?php echo $isDark ? 'inverse' : 'default'; ?> navbar-<?php echo $isCpct ? 'fixed' : 'static'; ?>-top"><?php
+if ($isCpct):
+?><div class="container"><?php
+endif;
+?>
 			<div class="navbar-header">
 				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
 					<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
@@ -75,25 +115,38 @@ endif;
 					<span class="icon-bar"></span>
 				</button>
 				<a class="reloadlink navbar-brand" href="">
-					<img alt="<?php echo I18n::_('PrivateBin'); ?>" src="img/icon.svg" width="38" />
+					<img alt="<?php echo I18n::_($NAME); ?>" src="img/icon.svg" width="38" />
 				</a>
 			</div>
 			<div id="navbar" class="navbar-collapse collapse">
 				<ul class="nav navbar-nav">
+					<li id="loadingindicator" class="navbar-text hidden">
+						<span class="glyphicon glyphicon-time" aria-hidden="true"></span>
+						<?php echo I18n::_('Loading…'), PHP_EOL; ?>
+					</li>
 					<li>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL; ?>
+<?php
+if ($isPage):
+?>
+						<button id="sendbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL;
+else:
+?>
+						<button id="newbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL;
+endif;
+?>
 						</button>
 <?php
 if ($EXPIRECLONE):
 ?>
-						<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
+						<button id="clonebutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
 							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
 						</button>
 <?php
 endif;
 ?>
-						<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
+						<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>
 					</li>
@@ -126,34 +179,91 @@ endforeach;
 ?>
 						</ul>
 					</li>
+<?php
+if ($isCpct):
+?>
+					<li id="formatter" class="dropdown">
+						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Options'); ?> <span class="caret"></span></a>
+						<ul class="dropdown-menu">
+							<li id="burnafterreadingoption" class="checkbox hidden">
+								<label>
+									<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
+    if ($BURNAFTERREADINGSELECTED):
+?> checked="checked"<?php
+    endif;
+?> />
+									<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
+								</label>
+							</li>
+<?php
+    if ($DISCUSSION):
+?>
+							<li id="opendiscussionoption" class="checkbox hidden">
+								<label>
+									<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
+        if ($OPENDISCUSSION):
+?> checked="checked"<?php
+        endif;
+?> />
+									<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
+								</label>
+							</li>
+<?php
+    endif;
+?>
+							<li role="separator" class="divider"></li>
+							<li>
+								<div>
+									<?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span>
+								</div>
+							</li>
+<?php
+    foreach ($FORMATTER as $key => $value):
+?>
+							<li>
+								<a href="#" data-format="<?php echo $key; ?>">
+									<?php echo $value, PHP_EOL; ?>
+								</a>
+							</li>
+<?php
+    endforeach;
+?>
+						</ul>
+						<select id="pasteFormatter" name="pasteFormatter" class="hidden">
+						</select>
+					</li>
+<?php
+else:
+?>
 					<li>
 						<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
 							<label>
 								<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-if ($BURNAFTERREADINGSELECTED):
+    if ($BURNAFTERREADINGSELECTED):
 ?> checked="checked"<?php
-endif;
+    endif;
 ?> />
 								<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
 							</label>
 						</div>
 					</li>
 <?php
-if ($DISCUSSION):
+    if ($DISCUSSION):
 ?>
 					<li>
-						<div id="opendisc" class="navbar-text checkbox hidden">
+						<div id="opendiscussionoption" class="navbar-text checkbox hidden">
 							<label>
 								<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-    if ($OPENDISCUSSION):
+        if ($OPENDISCUSSION):
 ?> checked="checked"<?php
-    endif;
+        endif;
 ?> />
 								<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
 						 	</label>
 						</div>
 					</li>
 <?php
+    endif;
 endif;
 if ($PASSWORD):
 ?>
@@ -174,6 +284,7 @@ if ($FILEUPLOAD):
 									<input type="file" id="file" name="file" />
 								</div>
 							</li>
+							<li id="customattachment" class="hidden"></li>
 							<li>
 								<a id="fileremovebutton"  href="#">
 									<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
@@ -183,25 +294,26 @@ if ($FILEUPLOAD):
 					</li>
 <?php
 endif;
+if (!$isCpct):
 ?>
 					<li class="dropdown">
 						<select id="pasteFormatter" name="pasteFormatter" class="hidden">
 <?php
-foreach ($FORMATTER as $key => $value):
+    foreach ($FORMATTER as $key => $value):
 ?>
 							<option value="<?php echo $key; ?>"<?php
-    if ($key == $FORMATTERDEFAULT):
+        if ($key == $FORMATTERDEFAULT):
 ?> selected="selected"<?php
-    endif;
+        endif;
 ?>><?php echo $value; ?></option>
 <?php
-endforeach;
+    endforeach;
 ?>
 						</select>
 						<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span></a>
 						<ul class="dropdown-menu">
 <?php
-foreach ($FORMATTER as $key => $value):
+    foreach ($FORMATTER as $key => $value):
 ?>
 							<li>
 								<a href="#" data-format="<?php echo $key; ?>">
@@ -209,10 +321,13 @@ foreach ($FORMATTER as $key => $value):
 								</a>
 							</li>
 <?php
-endforeach;
+    endforeach;
 ?>
 						</ul>
 					</li>
+<?php
+endif;
+?>
 				</ul>
 				<ul class="nav navbar-nav pull-right">
 <?php
@@ -238,100 +353,144 @@ if (strlen($LANGUAGESELECTION)):
 endif;
 ?>
 					<li>
-						<button id="sendbutton" type="button" class="hidden btn btn-primary navbar-btn">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL; ?>
+<?php
+if ($isPage):
+?>
+						<button id="newbutton" type="button" class="reloadlink hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL;
+else:
+?>
+						<button id="sendbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL;
+endif;
+?>
 						</button>
 					</li>
 				</ul>
 			</div>
-		</nav>
-		<header class="container">
+		<?php
+if ($isCpct):
+?></div><?php
+endif;
+?></nav>
+		<main>
+			<section class="container">
 <?php
 if (strlen($NOTICE)):
 ?>
-			<div role="alert" class="alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
-			</div>
+				<div role="alert" class="alert alert-info">
+					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
+					<?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
+				</div>
 <?php
 endif;
 ?>
-			<div id="remainingtime" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-			</div>
+				<div id="remainingtime" role="alert" class="hidden alert alert-info">
+					<span class="glyphicon glyphicon-fire" aria-hidden="true"></span>
+				</div>
 <?php
 if ($FILEUPLOAD):
 ?>
-			<div id="attachment" role="alert" class="hidden alert alert-info">
-				<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a><?php echo I18n::_('Download attachment'); ?></a> <span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-			</div>
-<?php
-endif;
-if (strlen($STATUS)):
-?>
-			<div id="status" role="alert" class="alert alert-success">
-				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
-			</div>
+				<div id="attachment" role="alert" class="hidden alert alert-info">
+					<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>
+					<a class="alert-link"><?php echo I18n::_('Download attachment'), PHP_EOL; ?></a>
+				</div>
 <?php
 endif;
 ?>
-			<div id="errormessage" role="alert" class="<?php
-if (!strlen($ERROR)):
-?>hidden <?php
-endif;
-?>alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo htmlspecialchars($ERROR); ?></div>
-			<noscript><div id="noscript" role="alert" class="nonworking alert alert-warning"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> <?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
-			<div id="ienotice" role="alert" class="hidden alert alert-warning"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> <?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
-				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
-			</div>
-			<div id="pasteresult" 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="status" role="alert" class="statusmessage alert alert-info<?php echo empty($STATUS) ? ' hidden' : '' ?>">
+					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
+					<?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
+				</div>
+				<div id="errormessage" role="alert" class="statusmessage<?php echo empty($ERROR) ? ' hidden' : '' ?> alert alert-danger">
+					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
+					<?php echo htmlspecialchars($ERROR), PHP_EOL; ?>
+				</div>
+				<noscript>
+					<div id="noscript" role="alert" class="nonworking alert alert-<?php echo $isDark ? 'error' : 'warning'; ?>">
+						<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+						<?php echo I18n::_('JavaScript is required for %s to work.<br />Sorry for the inconvenience.', I18n::_($NAME)), PHP_EOL; ?>
+					</div>
+				</noscript>
+				<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger">
+					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
+					<?php echo I18n::_('%s requires a modern browser to work.', I18n::_($NAME)), PHP_EOL; ?>
+				</div>
+				<div id="ienotice" role="alert" class="hidden alert alert-<?php echo $isDark ? 'error' : 'warning'; ?>">
+					<span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span>
+					<?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
+					<a href="https://www.mozilla.org/firefox/">Firefox</a>,
+					<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">
+					<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
+					<div id="deletelink"></div>
+					<div id="pastelink">
 <?php
 if (strlen($URLSHORTENER)):
 ?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-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>
-			</div>
-			<ul id="preview" class="nav nav-tabs hidden">
-				<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-				<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-			</ul>
-		</header>
-		<section class="container">
-			<article class="row">
-				<div id="image" class="col-md-12 text-center hidden"></div>
-				<div id="prettymessage" class="col-md-12 hidden">
-					<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
+				<ul id="editorTabs" class="nav nav-tabs hidden">
+					<li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
+					<li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
+				</ul>
+			</section>
+			<section class="container">
+				<article class="row">
+					<div id="placeholder" class="col-md-12 hidden"><?php echo I18n::_('+++ no paste text +++'); ?></div>
+					<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>
+					<div id="prettymessage" class="col-md-12 hidden">
+						<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
+					</div>
+					<div id="plaintext" class="col-md-12 hidden"></div>
+					<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
+				</article>
+			</section>
+			<section class="container">
+				<div id="discussion" class="hidden">
+					<h4><?php echo I18n::_('Discussion'); ?></h4>
+					<div id="commentcontainer"></div>
 				</div>
-				<div id="cleartext" class="col-md-12 hidden"></div>
-				<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
-			</article>
-		</section>
-		<section class="container">
-			<div id="discussion" class="hidden">
-				<h4><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
-			</div>
-		</section>
-		<footer class="container">
-			<div class="row">
-				<h4 class="col-md-5 col-xs-8"><?php echo I18n::_('PrivateBin'); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-				<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-				<p id="aboutbox" class="col-md-6 col-xs-12">
-					<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'), PHP_EOL; ?>
-				</p>
+			</section>
+			<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…'); ?><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>
+				</div>
+			</section>
+			<footer class="container">
+				<div class="row">
+					<h4 class="col-md-5 col-xs-8"><?php echo I18n::_($NAME); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
+					<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
+					<p id="aboutbox" class="col-md-6 col-xs-12">
+						<?php echo I18n::_('%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.', I18n::_($NAME)), PHP_EOL; ?>
+					</p>
+				</div>
+			</footer>
+		</main>
+		<div id="serverdata" class="hidden" aria-hidden="true">
+			<div id="cipherdata"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
+<?php
+if ($DISCUSSION):
+?>
+			<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>
-		</footer>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
+<?php
+endif;
+?>
+		</div>
 	</body>
 </html>

+ 40 - 20
tpl/page.php

@@ -6,7 +6,7 @@ use PrivateBin\I18n;
 		<meta charset="utf-8" />
 		<meta name="robots" content="noindex" />
 		<meta name="referrer" content="no-referrer">
-		<title><?php echo I18n::_('PrivateBin'); ?></title>
+		<title><?php echo I18n::_($NAME); ?></title>
 		<link type="text/css" rel="stylesheet" href="css/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
 <?php
 if ($SYNTAXHIGHLIGHTING):
@@ -20,8 +20,8 @@ if ($SYNTAXHIGHLIGHTING):
     endif;
 endif;
 ?>
-		<script type="text/javascript" src="js/jquery-1.11.3.js" integrity="sha512-xAERw8wHVOkM9x9y9/FvQ7g1t+gIQj39mfkN1Rd1OLcC5k/x2e6NO8hq6qEbb3oO+CYYTjVLFiFYg5/7ddF0zA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" src="js/sjcl-1.0.4.js" integrity="sha512-BqVQ8GgWfMCcdsDuP6Ggm1BV7+mmoWH3PC4UqcYpEKSdEq1rthy6NUsa6gu5sydewbi/ilI3E3ohdCxlPPF9ww==" crossorigin="anonymous"></script>
+		<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 ($ZEROBINCOMPATIBILITY):
 ?>
@@ -43,27 +43,27 @@ if ($SYNTAXHIGHLIGHTING):
 endif;
 if ($MARKDOWN):
 ?>
-		<script type="text/javascript" src="js/showdown-1.4.1.js" integrity="sha512-Kbz1FIlDnqUJu/3yW8H8USzURA3JuUqSKRwz13lM4kWt6C0n6s4tjl81PCfnWtE4gBIzyj5uGePcfUyotk/icw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/showdown-1.6.1.js" integrity="sha512-e6kAsBTgFnTBnEQXrq8BV6+XFwxb3kyWHeEPOl+KhxaWt3xImE2zAW2+yP3E2CQ7F9yoJl1poVU9qxkOEtVsTQ==" crossorigin="anonymous"></script>
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Jx4qD49amdzpNe9FLtxLO84Xt5LZeQ2PGaM0I9UCS2Kr4xhrnFyvP+0hrLIMgDTwjWFDpTSCoDHuj0SHzuqXuQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7/bLmiTErcH3lMJi1JxXk/lb20NUO7TCqasWeFybnNhnfOUnJ9GQGqPVJxPf+7fnw9dgLa1s18FXQuRanLD8Hw==" 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]-->
-		<link rel="apple-touch-icon" href="apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
-		<link rel="icon" type="image/png" href="favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
-		<link rel="icon" type="image/png" href="favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
+		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
+		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
+		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
 		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
-		<link rel="shortcut icon" href="favicon.ico">
+		<link rel="mask-icon" href="img/safari-pinned-tab.svg?<?php echo rawurlencode($VERSION); ?>" color="#ffcc00" />
+		<link rel="shortcut icon" href="img/favicon.ico">
 		<meta name="msapplication-config" content="browserconfig.xml">
 		<meta name="theme-color" content="#ffe57e" />
 	</head>
 	<body>
 		<header>
 			<div id="aboutbox">
-				<?php echo I18n::_('PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.'); ?><br />
+				<?php echo I18n::_('%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://privatebin.info/">project page</a>.', I18n::_($NAME)); ?><br />
 <?php
 if (strlen($NOTICE)):
 ?>
@@ -71,20 +71,20 @@ if (strlen($NOTICE)):
 endif;
 ?>
 			</div>
-			<h1 class="title reloadlink"><?php echo I18n::_('PrivateBin'); ?></h1><br />
+			<h1 class="title reloadlink"><?php echo I18n::_($NAME); ?></h1><br />
 			<h2 class="title"><?php echo I18n::_('Because ignorance is bliss'); ?></h2><br />
 			<h3 class="title"><?php echo $VERSION; ?></h3>
-			<noscript><div id="noscript" class="nonworking"><?php echo I18n::_('Javascript is required for PrivateBin to work.<br />Sorry for the inconvenience.'); ?></div></noscript>
-			<div id="oldienotice" class="nonworking"><?php echo I18n::_('PrivateBin requires a modern browser to work.'); ?></div>
+			<noscript><div id="noscript" class="nonworking"><?php echo I18n::_('JavaScript is required for %s to work.<br />Sorry for the inconvenience.', I18n::_($NAME)); ?></div></noscript>
+			<div id="oldienotice" class="nonworking"><?php echo I18n::_('%s requires a modern browser to work.', I18n::_($NAME)); ?></div>
 			<div id="ienotice"><?php echo I18n::_('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:'), PHP_EOL; ?>
 				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
 				<a href="https://www.opera.com/">Opera</a>,
-				<a href="https://www.google.com/chrome">Chrome</a>,
-				<a href="https://www.apple.com/safari">Safari</a>...
+				<a href="https://www.google.com/chrome">Chrome</a>…
 			</div>
 		</header>
 		<section>
 			<article>
+				<div id="loadingindicator" class="hidden"><?php echo I18n::_('Loading…'); ?></div>
 				<div id="status"><?php echo htmlspecialchars($STATUS); ?></div>
 				<div id="errormessage" class="hidden"><?php echo htmlspecialchars($ERROR); ?></div>
 				<div id="toolbar">
@@ -125,7 +125,7 @@ endif;
 <?php
 if ($DISCUSSION):
 ?>
-					<div id="opendisc" class="button hidden">
+					<div id="opendiscussionoption" class="button hidden">
 						<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
     if ($OPENDISCUSSION):
 ?> checked="checked"<?php
@@ -216,16 +216,36 @@ endif;
 				<div id="prettymessage" class="hidden">
 					<pre id="prettyprint" class="prettyprint linenums:1"></pre>
 				</div>
-				<div id="cleartext" class="hidden"></div>
+				<div id="plaintext" class="hidden"></div>
 				<textarea id="message" name="message" cols="80" rows="25" class="hidden"></textarea>
 			</article>
 		</section>
 		<section>
 			<div id="discussion" class="hidden">
 				<h4 class="title"><?php echo I18n::_('Discussion'); ?></h4>
-				<div id="comments"></div>
+				<div id="commentcontainer"></div>
+			</div>
+		</section>
+		<div id="serverdata" class="hidden" aria-hidden="true">
+			<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
+<?php
+if ($DISCUSSION):
+?>
+			<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>
+<?php
+endif;
+?>
+		</div>
+        <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>
 			</div>
 		</section>
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
 	</body>
 </html>

+ 13 - 13
tst/Bootstrap.php

@@ -36,12 +36,12 @@ class Helper
      * @var array
      */
     private static $paste = array(
-        'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
-        'attachment' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
+        'data'           => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
+        'attachment'     => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
         'attachmentname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
-        'meta' => array(
-            'formatter' => 'plaintext',
-            'postdate' => 1344803344,
+        'meta'           => array(
+            'formatter'      => 'plaintext',
+            'postdate'       => 1344803344,
             'opendiscussion' => true,
         ),
     );
@@ -62,7 +62,7 @@ class Helper
         'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
         'meta' => array(
             'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
-            'vizhash' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
+            'vizhash'  => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
             'postdate' => 1344803528,
         ),
     );
@@ -103,9 +103,9 @@ class Helper
      */
     public static function getPasteWithAttachment($meta = array())
     {
-        $example = self::$paste;
+        $example                 = self::$paste;
         $example['meta']['salt'] = ServerSalt::generate();
-        $example['meta'] = array_merge($example['meta'], $meta);
+        $example['meta']         = array_merge($example['meta'], $meta);
         return $example;
     }
 
@@ -122,10 +122,10 @@ class Helper
         if (count($meta)) {
             $example['meta'] = $meta;
         }
-        $example['comments'] = array();
-        $example['comment_count'] = 0;
+        $example['comments']       = array();
+        $example['comment_count']  = 0;
         $example['comment_offset'] = 0;
-        $example['@context'] = 'js/paste.jsonld';
+        $example['@context']       = 'js/paste.jsonld';
         return json_encode($example);
     }
 
@@ -146,7 +146,7 @@ class Helper
      */
     public static function getComment($meta = array())
     {
-        $example = self::$comment;
+        $example         = self::$comment;
         $example['meta'] = array_merge($example['meta'], $meta);
         return $example;
     }
@@ -158,7 +158,7 @@ class Helper
      */
     public static function getCommentPost($meta = array())
     {
-        $example = self::getComment($meta);
+        $example             = self::getComment($meta);
         $example['nickname'] = $example['meta']['nickname'];
         unset($example['meta']['nickname']);
         return $example;

+ 16 - 16
tst/ConfigurationTest.php

@@ -12,11 +12,11 @@ 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'];
-        $this->_minimalConfig = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]';
+        $this->_options['traffic']['dir']       = PATH . $this->_options['traffic']['dir'];
+        $this->_options['purge']['dir']         = PATH . $this->_options['purge']['dir'];
+        $this->_minimalConfig                   = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]';
     }
 
     public function tearDown()
@@ -94,19 +94,19 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase
 
     public function testHandleWrongTypes()
     {
-        $original_options = $this->_options;
+        $original_options                                    = $this->_options;
         $original_options['main']['syntaxhighlightingtheme'] = 'foo';
-        $options = $original_options;
-        $options['main']['discussion'] = 'true';
-        $options['main']['opendiscussion'] = 0;
-        $options['main']['password'] = -1; // evaluates to TRUE
-        $options['main']['fileupload'] = 'false';
-        $options['expire_options']['foo'] = 'bar';
-        $options['formatter_options'][] = 'foo';
+        $options                                             = $original_options;
+        $options['main']['discussion']                       = 'true';
+        $options['main']['opendiscussion']                   = 0;
+        $options['main']['password']                         = -1; // evaluates to TRUE
+        $options['main']['fileupload']                       = 'false';
+        $options['expire_options']['foo']                    = 'bar';
+        $options['formatter_options'][]                      = 'foo';
         Helper::createIniFile(CONF, $options);
-        $conf = new Configuration;
+        $conf                                      = new Configuration;
         $original_options['expire_options']['foo'] = intval('bar');
-        $original_options['formatter_options'][0] = 'foo';
+        $original_options['formatter_options'][0]  = 'foo';
         $this->assertEquals($original_options, $conf->get(), 'incorrect types are corrected');
     }
 
@@ -117,14 +117,14 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase
         unset($options['expire_options']['1year']);
         unset($options['expire_options']['never']);
         Helper::createIniFile(CONF, $options);
-        $conf = new Configuration;
+        $conf                         = new Configuration;
         $options['expire']['default'] = '5min';
         $this->assertEquals($options, $conf->get(), 'not overriding "missing" subkeys');
     }
 
     public function testHandlePreRenameConfig()
     {
-        $options = $this->_options;
+        $options                   = $this->_options;
         $options['model']['class'] = 'zerobin_data';
         Helper::createIniFile(CONF, $options);
         $conf = new Configuration;

+ 82 - 83
tst/ConfigurationTestGenerator.php

@@ -9,7 +9,6 @@
  * DANGER: Too many options/settings and too high max iteration setting may trigger
  *         a fork bomb. Please save your work before executing this script.
  */
-
 include 'Bootstrap.php';
 
 $vrd  = array('view', 'read', 'delete');
@@ -19,85 +18,85 @@ new ConfigurationTestGenerator(array(
     'main/discussion' => array(
         array(
             'setting' => true,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => $vrd),
-                    'type' => 'RegExp',
-                    'args' => array(
+                    'type'       => 'RegExp',
+                    'args'       => array(
                         '#<div[^>]*id="opendisc"[^>]*>#',
                         '$content',
-                        'outputs enabled discussion correctly'
+                        'outputs enabled discussion correctly',
                     ),
                 ), array(
                     'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
-                    'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'settings'   => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
+                    'type'       => 'Equals',
+                    'args'       => array(
                         1,
                         '$response["status"]',
-                        'when discussions are enabled, but invalid flag posted, fail to create paste'
+                        'when discussions are enabled, but invalid flag posted, fail to create paste',
                     ),
                 ), array(
                     'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
-                    'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
-                    'type' => 'False',
-                    'args' => array(
+                    'settings'   => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
+                    'type'       => 'False',
+                    'args'       => array(
                         '$this->_model->exists(Helper::getPasteId())',
-                        'when discussions are enabled, but invalid flag posted, paste is not created'
+                        'when discussions are enabled, but invalid flag posted, paste is not created',
                     ),
                 ),
             ),
-            'affects' => $vcud
+            'affects' => $vcud,
         ), array(
             'setting' => false,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'NotRegExp',
                     'args' => array(
                         '#<div[^>]*id="opendisc"[^>]*>#',
                         '$content',
-                        'outputs disabled discussion correctly'
+                        'outputs disabled discussion correctly',
                     ),
                 ),
             ),
-            'affects' => $vrd
+            'affects' => $vrd,
         ),
     ),
     'main/opendiscussion' => array(
         array(
             'setting' => true,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('main/discussion' => true),
-                    'type' => 'RegExp',
-                    'args' => array(
+                    'type'       => 'RegExp',
+                    'args'       => array(
                         '#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
                         '$content',
-                        'outputs checked discussion correctly'
+                        'outputs checked discussion correctly',
                     ),
                 ),
             ),
-            'affects' => $vrd
+            'affects' => $vrd,
         ), array(
             'setting' => false,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('main/discussion' => true),
-                    'type' => 'NotRegExp',
-                    'args' => array(
+                    'type'       => 'NotRegExp',
+                    'args'       => array(
                         '#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
                         '$content',
-                        'outputs unchecked discussion correctly'
+                        'outputs unchecked discussion correctly',
                     ),
                 ),
             ),
-            'affects' => $vrd
+            'affects' => $vrd,
         ),
     ),
     'main/burnafterreadingselected' => array(
         array(
             'setting' => true,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'RegExp',
                     'args' => array(
@@ -110,7 +109,7 @@ new ConfigurationTestGenerator(array(
             'affects' => array('view'),
         ), array(
             'setting' => false,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'NotRegExp',
                     'args' => array(
@@ -126,37 +125,37 @@ new ConfigurationTestGenerator(array(
     'main/password' => array(
         array(
             'setting' => true,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'RegExp',
                     'args' => array(
                         '#<div[^>]*id="password"[^>]*>#',
                         '$content',
-                        'outputs password input correctly'
+                        'outputs password input correctly',
                     ),
                 ),
             ),
-            'affects' => $vrd
+            'affects' => $vrd,
         ), array(
             'setting' => false,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('main/discussion' => true),
-                    'type' => 'NotRegExp',
-                    'args' => array(
+                    'type'       => 'NotRegExp',
+                    'args'       => array(
                         '#<div[^>]*id="password"[^>]*>#',
                         '$content',
-                        'removes password input correctly'
+                        'removes password input correctly',
                     ),
                 ),
             ),
-            'affects' => $vrd
+            'affects' => $vrd,
         ),
     ),
     'main/template' => array(
         array(
             'setting' => 'page',
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'RegExp',
                     'args' => array(
@@ -176,7 +175,7 @@ new ConfigurationTestGenerator(array(
             'affects' => $vrd,
         ), array(
             'setting' => 'bootstrap',
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'type' => 'NotRegExp',
                     'args' => array(
@@ -199,102 +198,102 @@ new ConfigurationTestGenerator(array(
     'main/sizelimit' => array(
         array(
             'setting' => 10,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'type'       => 'Equals',
+                    'args'       => array(
                         1,
                         '$response["status"]',
-                        'when sizelimit limit exceeded, fail to create paste'
+                        'when sizelimit limit exceeded, fail to create paste',
                     ),
                 ),
             ),
-            'affects' => array('create')
+            'affects' => array('create'),
         ), array(
             'setting' => 2097152,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
-                    'settings' => array('sleep(3)'),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'settings'   => array('sleep(3)'),
+                    'type'       => 'Equals',
+                    'args'       => array(
                         0,
                         '$response["status"]',
-                        'when sizelimit limit is not reached, successfully create paste'
+                        'when sizelimit limit is not reached, successfully create paste',
                     ),
                 ), array(
                     'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
-                    'settings' => array('sleep(3)'),
-                    'type' => 'True',
-                    'args' => array(
+                    'settings'   => array('sleep(3)'),
+                    'type'       => 'True',
+                    'args'       => array(
                         '$this->_model->exists($response["id"])',
-                        'when sizelimit limit is not reached, paste exists after posting data'
+                        'when sizelimit limit is not reached, paste exists after posting data',
                     ),
                 ),
             ),
-            'affects' => array('create')
+            'affects' => array('create'),
         ),
     ),
     'traffic/limit' => array(
         array(
             'setting' => 0,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'type'       => 'Equals',
+                    'args'       => array(
                         0,
                         '$response["status"]',
-                        'when traffic limit is disabled, successfully create paste'
+                        'when traffic limit is disabled, successfully create paste',
                     ),
                 ), array(
                     'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
-                    'type' => 'True',
-                    'args' => array(
+                    'type'       => 'True',
+                    'args'       => array(
                         '$this->_model->exists($response["id"])',
-                        'when traffic limit is disabled, paste exists after posting data'
+                        'when traffic limit is disabled, paste exists after posting data',
                     ),
                 ),
             ),
-            'affects' => array('create')
+            'affects' => array('create'),
         ), array(
             'setting' => 10,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => array('create')),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'type'       => 'Equals',
+                    'args'       => array(
                         1,
                         '$response["status"]',
-                        'when traffic limit is on and we do not wait, fail to create paste'
+                        'when traffic limit is on and we do not wait, fail to create paste',
                     ),
                 ),
             ),
-            'affects' => array('create')
+            'affects' => array('create'),
         ), array(
             'setting' => 2,
-            'tests' => array(
+            'tests'   => array(
                 array(
                     'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
-                    'settings' => array('sleep(3)'),
-                    'type' => 'Equals',
-                    'args' => array(
+                    'settings'   => array('sleep(3)'),
+                    'type'       => 'Equals',
+                    'args'       => array(
                         0,
                         '$response["status"]',
-                        'when traffic limit is on and we wait, successfully create paste'
+                        'when traffic limit is on and we wait, successfully create paste',
                     ),
                 ), array(
                     'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
-                    'settings' => array('sleep(3)'),
-                    'type' => 'True',
-                    'args' => array(
+                    'settings'   => array('sleep(3)'),
+                    'type'       => 'True',
+                    'args'       => array(
                         '$this->_model->exists($response["id"])',
-                        'when traffic limit is on and we wait, paste exists after posting data'
+                        'when traffic limit is on and we wait, paste exists after posting data',
                     ),
                 ),
             ),
-            'affects' => array('create')
+            'affects' => array('create'),
         ),
     ),
 ));
@@ -325,7 +324,7 @@ class ConfigurationTestGenerator
      * @var array
      */
     private $_configurations = array(
-        array('options' => array(), 'tests' => array(), 'affects' => array())
+        array('options' => array(), 'tests' => array(), 'affects' => array()),
     );
 
     /**
@@ -346,10 +345,10 @@ class ConfigurationTestGenerator
     private function _writeConfigurationTest()
     {
         $defaultOptions = parse_ini_file(CONF, true);
-        $code = $this->_getHeader();
+        $code           = $this->_getHeader();
         foreach ($this->_configurations as $key => $conf) {
             $fullOptions = array_replace_recursive($defaultOptions, $conf['options']);
-            $options = Helper::varExportMin($fullOptions, true);
+            $options     = Helper::varExportMin($fullOptions, true);
             foreach ($conf['affects'] as $step) {
                 $testCode = $preCode = array();
                 foreach ($conf['tests'] as $tests) {
@@ -470,7 +469,7 @@ EOT;
     private function _getFunction($step, $key, &$options, $preCode, $testCode)
     {
         if (count($testCode) == 0) {
-            echo "skipping creation of test$step$key, no valid tests found for configuration: $options". PHP_EOL;
+            echo "skipping creation of test$step$key, no valid tests found for configuration: $options" . PHP_EOL;
             return '';
         }
 
@@ -593,7 +592,7 @@ EOT;
 
             // create clones for each of the other settings
             while ($setting = next($settings)) {
-                $clone = $this->_configurations[$c];
+                $clone                   = $this->_configurations[$c];
                 $this->_configurations[] = $this->_addSetting($clone, $setting, $section, $option);
             }
             reset($settings);
@@ -626,7 +625,7 @@ EOT;
             throw new Exception("Endless loop or error in options detected: option '$option' already exists with setting '$val' in one of the configurations!");
         }
         $configuration['options'][$section][$option] = $setting['setting'];
-        $configuration['tests'][$option] = array($setting['tests'], $setting['affects']);
+        $configuration['tests'][$option]             = array($setting['tests'], $setting['affects']);
         foreach ($setting['affects'] as $affects) {
             if (!in_array($affects, $configuration['affects'])) {
                 $configuration['affects'][] = $affects;

+ 34 - 25
tst/Data/DatabaseTest.php

@@ -19,7 +19,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     public function setUp()
     {
         /* Setup Routine */
-        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_model = Database::getInstance($this->_options);
     }
 
@@ -47,8 +47,8 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()) !== false, 'store comment');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
-        $comment = json_decode(json_encode(Helper::getComment()));
-        $comment->id = Helper::getCommentId();
+        $comment           = json_decode(json_encode(Helper::getComment()));
+        $comment->id       = Helper::getCommentId();
         $comment->parentid = Helper::getPasteId();
         $this->assertEquals(
             array($comment->meta->postdate => $comment),
@@ -65,10 +65,10 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     public function testDatabaseBasedAttachmentStoreWorks()
     {
         $this->_model->delete(Helper::getPasteId());
-        $original = $paste = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
+        $original                          = $paste                          = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
         $paste['meta']['burnafterreading'] = $original['meta']['burnafterreading'] = true;
-        $paste['meta']['attachment'] = $paste['attachment'];
-        $paste['meta']['attachmentname'] = $paste['attachmentname'];
+        $paste['meta']['attachment']       = $paste['attachment'];
+        $paste['meta']['attachmentname']   = $paste['attachmentname'];
         unset($paste['attachment'], $paste['attachmentname']);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste');
@@ -77,19 +77,24 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(json_decode(json_encode($original)), $this->_model->read(Helper::getPasteId()));
     }
 
+    /**
+     * pastes a-g are expired and should get deleted, x never expires and y-z expire in an hour
+     */
     public function testPurge()
     {
         $this->_model->delete(Helper::getPasteId());
         $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z');
-        $ids = array();
+        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
+        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z');
+        $ids     = array();
         foreach ($keys as $key) {
             $ids[$key] = substr(md5($key), 0, 16);
             $this->_model->delete($ids[$key]);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, array('y', 'z'))) {
                 $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste");
+            } elseif ($key === 'x') {
+                $this->assertTrue($this->_model->create($ids[$key], Helper::getPaste()), "store $key paste");
             } else {
                 $this->assertTrue($this->_model->create($ids[$key], $expired), "store $key paste");
             }
@@ -113,7 +118,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'ibm:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -124,7 +129,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'informix:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -135,7 +140,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'mssql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -146,7 +151,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'mysql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -157,7 +162,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'oci:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -168,7 +173,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         Database::getInstance(array(
             'dsn' => 'pgsql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
     }
 
@@ -179,7 +184,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     public function testGetFooInstance()
     {
         Database::getInstance(array(
-            'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null
+            'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null,
         ));
     }
 
@@ -231,13 +236,15 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         mkdir($this->_path);
         $path = $this->_path . DIRECTORY_SEPARATOR . 'attachement-test.sq3';
-        if (is_file($path)) unlink($path);
+        if (is_file($path)) {
+            unlink($path);
+        }
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['tbl'] = 'bar_';
-        $model = Database::getInstance($this->_options);
+        $model                 = Database::getInstance($this->_options);
 
-        $original = $paste = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
-        $paste['meta']['attachment'] = $paste['attachment'];
+        $original                        = $paste                        = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
+        $paste['meta']['attachment']     = $paste['attachment'];
         $paste['meta']['attachmentname'] = $paste['attachmentname'];
         unset($paste['attachment'], $paste['attachmentname']);
         $meta = $paste['meta'];
@@ -274,10 +281,12 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
     {
         mkdir($this->_path);
         $path = $this->_path . DIRECTORY_SEPARATOR . 'db-test.sq3';
-        if (is_file($path)) unlink($path);
+        if (is_file($path)) {
+            unlink($path);
+        }
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['tbl'] = 'foo_';
-        $db = new PDO(
+        $db                    = new PDO(
             $this->_options['dsn'],
             $this->_options['usr'],
             $this->_options['pwd'],
@@ -294,13 +303,13 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
         );
         $db->exec(
             'CREATE TABLE foo_comment ( ' .
-            "dataid CHAR(16) NOT NULL, " .
+            'dataid CHAR(16) NOT NULL, ' .
             'pasteid CHAR(16), ' .
             'parentid CHAR(16), ' .
             'data BLOB, ' .
             'nickname BLOB, ' .
             'vizhash BLOB, ' .
-            "postdate INT );"
+            'postdate INT );'
         );
         $this->assertInstanceOf(Database::class, Database::getInstance($this->_options));
 

+ 13 - 8
tst/Data/FilesystemTest.php

@@ -11,7 +11,7 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
     public function setUp()
     {
         /* Setup Routine */
-        $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));
     }
 
@@ -37,8 +37,8 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'store comment');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
-        $comment = json_decode(json_encode(Helper::getComment()));
-        $comment->id = Helper::getCommentId();
+        $comment           = json_decode(json_encode(Helper::getComment()));
+        $comment->id       = Helper::getCommentId();
         $comment->parentid = Helper::getPasteId();
         $this->assertEquals(
             array($comment->meta->postdate => $comment),
@@ -55,8 +55,8 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
     public function testFileBasedAttachmentStoreWorks()
     {
         $this->_model->delete(Helper::getPasteId());
-        $original = $paste = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
-        $paste['meta']['attachment'] = $paste['attachment'];
+        $original                        = $paste                        = Helper::getPasteWithAttachment(array('expire_date' => 1344803344));
+        $paste['meta']['attachment']     = $paste['attachment'];
         $paste['meta']['attachmentname'] = $paste['attachmentname'];
         unset($paste['attachment'], $paste['attachmentname']);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
@@ -66,18 +66,23 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(json_decode(json_encode($original)), $this->_model->read(Helper::getPasteId()));
     }
 
+    /**
+     * pastes a-g are expired and should get deleted, x never expires and y-z expire in an hour
+     */
     public function testPurge()
     {
         mkdir($this->_path . DIRECTORY_SEPARATOR . '00', 0777, true);
         $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z');
-        $ids = array();
+        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
+        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z');
+        $ids     = array();
         foreach ($keys as $key) {
             $ids[$key] = substr(md5($key), 0, 16);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
             if (in_array($key, array('x', 'y', 'z'))) {
                 $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste");
+            } elseif ($key === 'x') {
+                $this->assertTrue($this->_model->create($ids[$key], Helper::getPaste()), "store $key paste");
             } else {
                 $this->assertTrue($this->_model->create($ids[$key], $expired), "store $key paste");
             }

+ 0 - 8
tst/FilterTest.php

@@ -4,14 +4,6 @@ use PrivateBin\Filter;
 
 class FilterTest extends PHPUnit_Framework_TestCase
 {
-    public function testFilterStripsSlashesDeeply()
-    {
-        $this->assertEquals(
-            array("f'oo", "b'ar", array("fo'o", "b'ar")),
-            Filter::stripslashesDeep(array("f\\'oo", "b\\'ar", array("fo\\'o", "b\\'ar")))
-        );
-    }
-
     public function testFilterMakesTimesHumanlyReadable()
     {
         $this->assertEquals('5 minutes', Filter::formatHumanReadableTime('5min'));

+ 74 - 11
tst/I18nTest.php

@@ -23,7 +23,7 @@ class I18nTest extends PHPUnit_Framework_TestCase
     public function testTranslationFallback()
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
-        $messageId = 'It does not matter if the message ID exists';
+        $messageId                       = 'It does not matter if the message ID exists';
         I18n::loadTranslations();
         $this->assertEquals($messageId, I18n::_($messageId), 'fallback to en');
     }
@@ -33,9 +33,9 @@ class I18nTest extends PHPUnit_Framework_TestCase
         $_COOKIE['lang'] = 'de';
         I18n::loadTranslations();
         $this->assertEquals($this->_translations['en'], I18n::_('en'), 'browser language de');
-        $this->assertEquals('0 Stunden', I18n::_('%d hours', 0), '0 hours in german');
-        $this->assertEquals('1 Stunde',  I18n::_('%d hours', 1), '1 hour in german');
-        $this->assertEquals('2 Stunden', I18n::_('%d hours', 2), '2 hours in french');
+        $this->assertEquals('0 Stunden', I18n::_('%d hours', 0), '0 hours in German');
+        $this->assertEquals('1 Stunde',  I18n::_('%d hours', 1), '1 hour in German');
+        $this->assertEquals('2 Stunden', I18n::_('%d hours', 2), '2 hours in German');
     }
 
     public function testBrowserLanguageDeDetection()
@@ -43,9 +43,9 @@ class I18nTest extends PHPUnit_Framework_TestCase
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'de-CH,de;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         I18n::loadTranslations();
         $this->assertEquals($this->_translations['en'], I18n::_('en'), 'browser language de');
-        $this->assertEquals('0 Stunden', I18n::_('%d hours', 0), '0 hours in german');
-        $this->assertEquals('1 Stunde',  I18n::_('%d hours', 1), '1 hour in german');
-        $this->assertEquals('2 Stunden', I18n::_('%d hours', 2), '2 hours in french');
+        $this->assertEquals('0 Stunden', I18n::_('%d hours', 0), '0 hours in German');
+        $this->assertEquals('1 Stunde',  I18n::_('%d hours', 1), '1 hour in German');
+        $this->assertEquals('2 Stunden', I18n::_('%d hours', 2), '2 hours in German');
     }
 
     public function testBrowserLanguageFrDetection()
@@ -53,9 +53,39 @@ class I18nTest extends PHPUnit_Framework_TestCase
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr-CH,fr;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         I18n::loadTranslations();
         $this->assertEquals('fr', I18n::_('en'), 'browser language fr');
-        $this->assertEquals('0 heure',  I18n::_('%d hours', 0), '0 hours in french');
-        $this->assertEquals('1 heure',  I18n::_('%d hours', 1), '1 hour in french');
-        $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in french');
+        $this->assertEquals('0 heure',  I18n::_('%d hours', 0), '0 hours in French');
+        $this->assertEquals('1 heure',  I18n::_('%d hours', 1), '1 hour in French');
+        $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in French');
+    }
+
+    public function testBrowserLanguageNoDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'no;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('no', I18n::_('en'), 'browser language no');
+        $this->assertEquals('0 timer',  I18n::_('%d hours', 0), '0 hours in Norwegian');
+        $this->assertEquals('1 time',  I18n::_('%d hours', 1), '1 hour in Norwegian');
+        $this->assertEquals('2 timer', I18n::_('%d hours', 2), '2 hours in Norwegian');
+    }
+
+    public function testBrowserLanguageOcDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'oc;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('oc', I18n::_('en'), 'browser language oc');
+        $this->assertEquals('0 ora',  I18n::_('%d hours', 0), '0 hours in Occitan');
+        $this->assertEquals('1 ora',  I18n::_('%d hours', 1), '1 hour in Occitan');
+        $this->assertEquals('2 oras', I18n::_('%d hours', 2), '2 hours in Occitan');
+    }
+
+    public function testBrowserLanguageZhDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'zh;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('zh', I18n::_('en'), 'browser language zh');
+        $this->assertEquals('0 小时',  I18n::_('%d hours', 0), '0 hours in Chinese');
+        $this->assertEquals('1 小时',  I18n::_('%d hours', 1), '1 hour in Chinese');
+        $this->assertEquals('2 小时', I18n::_('%d hours', 2), '2 hours in Chinese');
     }
 
     public function testBrowserLanguagePlDetection()
@@ -63,7 +93,40 @@ class I18nTest extends PHPUnit_Framework_TestCase
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'pl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         I18n::loadTranslations();
         $this->assertEquals('pl', I18n::_('en'), 'browser language pl');
-        $this->assertEquals('2 godzina', I18n::_('%d hours', 2), 'hours in polish');
+        $this->assertEquals('1 godzina', I18n::_('%d hours', 1), '1 hour in Polish');
+        $this->assertEquals('2 godzina', I18n::_('%d hours', 2), '2 hours in Polish');
+        $this->assertEquals('12 godzinę', I18n::_('%d hours', 12), '12 hours in Polish');
+        $this->assertEquals('22 godzina', I18n::_('%d hours', 22), '22 hours in Polish');
+        $this->assertEquals('1 minut',  I18n::_('%d minutes', 1), '1 minute in Polish');
+        $this->assertEquals('3 minut',  I18n::_('%d minutes', 3), '3 minutes in Polish');
+        $this->assertEquals('13 minut',  I18n::_('%d minutes', 13), '13 minutes in Polish');
+        $this->assertEquals('23 minut',  I18n::_('%d minutes', 23), '23 minutes in Polish');
+    }
+
+    public function testBrowserLanguageRuDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'ru;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('ru', I18n::_('en'), 'browser language ru');
+        $this->assertEquals('1 минуту',  I18n::_('%d minutes', 1), '1 minute in Russian');
+        $this->assertEquals('3 минуты',  I18n::_('%d minutes', 3), '3 minutes in Russian');
+        $this->assertEquals('10 минут',  I18n::_('%d minutes', 10), '10 minutes in Russian');
+        $this->assertEquals('21 минуту',  I18n::_('%d minutes', 21), '21 minutes in Russian');
+    }
+
+    public function testBrowserLanguageSlDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'sl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('sl', I18n::_('en'), 'browser language sl');
+        $this->assertEquals('0 ura',  I18n::_('%d hours', 0), '0 hours in Slowene');
+        $this->assertEquals('1 uri',  I18n::_('%d hours', 1), '1 hour in Slowene');
+        $this->assertEquals('2 ure', I18n::_('%d hours', 2), '2 hours in Slowene');
+        $this->assertEquals('3 ur',  I18n::_('%d hours', 3), '3 hours in Slowene');
+        $this->assertEquals('11 ura',  I18n::_('%d hours', 11), '11 hours in Slowene');
+        $this->assertEquals('101 uri',  I18n::_('%d hours', 101), '101 hours in Slowene');
+        $this->assertEquals('102 ure', I18n::_('%d hours', 102), '102 hours in Slowene');
+        $this->assertEquals('104 ur',  I18n::_('%d hours', 104), '104 hours in Slowene');
     }
 
     public function testBrowserLanguageAnyDetection()

+ 23 - 24
tst/JsonApiTest.php

@@ -1,9 +1,9 @@
 <?php
 
 use PrivateBin\Data\Filesystem;
+use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\PrivateBin;
 use PrivateBin\Request;
-use PrivateBin\Persistence\ServerSalt;
 
 class JsonApiTest extends PHPUnit_Framework_TestCase
 {
@@ -15,7 +15,7 @@ class JsonApiTest 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);
         $this->reset();
@@ -30,16 +30,16 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
 
     public function reset()
     {
-        $_POST = array();
-        $_GET = array();
+        $_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['purge']['dir'] = $this->_path;
-        $options['traffic']['dir'] = $this->_path;
+        $options                         = parse_ini_file(CONF, true);
+        $options['purge']['dir']         = $this->_path;
+        $options['traffic']['dir']       = $this->_path;
         $options['model_options']['dir'] = $this->_path;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
@@ -51,14 +51,14 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
     public function testCreate()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
+        $_POST                            = Helper::getPaste();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -81,7 +81,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
     public function testPut()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
@@ -90,10 +90,10 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         $file = tempnam(sys_get_temp_dir(), 'FOO');
         file_put_contents($file, http_build_query($paste));
         Request::setInputStream($file);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'PUT';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'PUT';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -120,14 +120,14 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         $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());
-        $file = tempnam(sys_get_temp_dir(), 'FOO');
+        $file  = tempnam(sys_get_temp_dir(), 'FOO');
         file_put_contents($file, http_build_query(array(
             'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt),
         )));
         Request::setInputStream($file);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'DELETE';
+        $_SERVER['REQUEST_METHOD']        = 'DELETE';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -147,12 +147,11 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $paste = $this->_model->read(Helper::getPasteId());
         $_POST = array(
-            'action' => 'delete',
+            'pasteid'     => Helper::getPasteId(),
             'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt),
         );
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -168,13 +167,13 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
     public function testRead()
     {
         $this->reset();
-        $paste = Helper::getPasteWithAttachment();
-        $paste['meta']['attachment'] = $paste['attachment'];
+        $paste                           = Helper::getPasteWithAttachment();
+        $paste['meta']['attachment']     = $paste['attachment'];
         $paste['meta']['attachmentname'] = $paste['attachmentname'];
         unset($paste['attachment']);
         unset($paste['attachmentname']);
         $this->_model->create(Helper::getPasteId(), $paste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;

+ 31 - 29
tst/ModelTest.php

@@ -1,5 +1,6 @@
 <?php
 
+use Identicon\Identicon;
 use PrivateBin\Configuration;
 use PrivateBin\Data\Database;
 use PrivateBin\Model;
@@ -7,7 +8,6 @@ use PrivateBin\Model\Paste;
 use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Persistence\TrafficLimiter;
 use PrivateBin\Vizhash16x16;
-use Identicon\Identicon;
 
 class ModelTest extends PHPUnit_Framework_TestCase
 {
@@ -22,11 +22,13 @@ class ModelTest extends PHPUnit_Framework_TestCase
         /* Setup Routine */
         Helper::confRestore();
         $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        if (!is_dir($this->_path)) mkdir($this->_path);
+        if (!is_dir($this->_path)) {
+            mkdir($this->_path);
+        }
         ServerSalt::setPath($this->_path);
-        $options = parse_ini_file(CONF, true);
+        $options                   = parse_ini_file(CONF, true);
         $options['purge']['limit'] = 0;
-        $options['model'] = array(
+        $options['model']          = array(
             'class' => 'Database',
         );
         $options['model_options'] = array(
@@ -37,8 +39,8 @@ class ModelTest extends PHPUnit_Framework_TestCase
         );
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $this->_conf = new Configuration;
-        $this->_model = new Model($this->_conf);
+        $this->_conf            = new Configuration;
+        $this->_model           = new Model($this->_conf);
         $_SERVER['REMOTE_ADDR'] = '::1';
     }
 
@@ -73,8 +75,8 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
         // storing comments
         $commentData = Helper::getComment();
-        $paste = $this->_model->getPaste(Helper::getPasteId());
-        $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId());
+        $paste       = $this->_model->getPaste(Helper::getPasteId());
+        $comment     = $paste->getComment(Helper::getPasteId(), Helper::getCommentId());
         $this->assertFalse($comment->exists(), 'comment does not yet exist');
 
         $comment = $paste->getComment(Helper::getPasteId());
@@ -123,7 +125,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
      */
     public function testCommentDuplicate()
     {
-        $pasteData = Helper::getPaste();
+        $pasteData   = Helper::getPaste();
         $commentData = Helper::getComment();
         $this->_model->getPaste(Helper::getPasteId())->delete();
 
@@ -146,7 +148,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
     public function testImplicitDefaults()
     {
-        $pasteData = Helper::getPaste();
+        $pasteData   = Helper::getPaste();
         $commentData = Helper::getComment();
         $this->_model->getPaste(Helper::getPasteId())->delete();
 
@@ -175,8 +177,8 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $comment->store();
 
         $identicon = new Identicon();
-        $pngdata = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
-        $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId())->get();
+        $pngdata   = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
+        $comment   = $paste->getComment(Helper::getPasteId(), Helper::getCommentId())->get();
         $this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set');
     }
 
@@ -230,13 +232,13 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
     public function testPurge()
     {
-        $conf = new Configuration;
+        $conf  = new Configuration;
         $store = Database::getInstance($conf->getSection('model_options'));
         $store->delete(Helper::getPasteId());
         $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z');
-        $ids = array();
+        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
+        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z');
+        $ids     = array();
         foreach ($keys as $key) {
             $ids[$key] = substr(md5($key), 0, 16);
             $store->delete($ids[$key]);
@@ -261,9 +263,9 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
     public function testCommentWithDisabledVizhash()
     {
-        $options = parse_ini_file(CONF, true);
+        $options                 = parse_ini_file(CONF, true);
         $options['main']['icon'] = 'none';
-        $options['model'] = array(
+        $options['model']        = array(
             'class' => 'Database',
         );
         $options['model_options'] = array(
@@ -297,8 +299,8 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
         // storing comments
         $commentData = Helper::getComment();
-        $paste = $model->getPaste(Helper::getPasteId());
-        $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId());
+        $paste       = $model->getPaste(Helper::getPasteId());
+        $comment     = $paste->getComment(Helper::getPasteId(), Helper::getCommentId());
         $this->assertFalse($comment->exists(), 'comment does not yet exist');
 
         $comment = $paste->getComment(Helper::getPasteId());
@@ -316,9 +318,9 @@ class ModelTest extends PHPUnit_Framework_TestCase
 
     public function testCommentIdenticon()
     {
-        $options = parse_ini_file(CONF, true);
+        $options                 = parse_ini_file(CONF, true);
         $options['main']['icon'] = 'identicon';
-        $options['model'] = array(
+        $options['model']        = array(
             'class' => 'Database',
         );
         $options['model_options'] = array(
@@ -331,7 +333,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 
-        $pasteData = Helper::getPaste();
+        $pasteData   = Helper::getPaste();
         $commentData = Helper::getComment();
         $model->getPaste(Helper::getPasteId())->delete();
 
@@ -347,16 +349,16 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $comment->store();
 
         $identicon = new Identicon();
-        $pngdata = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
-        $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId())->get();
+        $pngdata   = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
+        $comment   = $paste->getComment(Helper::getPasteId(), Helper::getCommentId())->get();
         $this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set');
     }
 
     public function testCommentVizhash()
     {
-        $options = parse_ini_file(CONF, true);
+        $options                 = parse_ini_file(CONF, true);
         $options['main']['icon'] = 'vizhash';
-        $options['model'] = array(
+        $options['model']        = array(
             'class' => 'Database',
         );
         $options['model_options'] = array(
@@ -369,7 +371,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 
-        $pasteData = Helper::getPaste();
+        $pasteData   = Helper::getPaste();
         $commentData = Helper::getComment();
         $model->getPaste(Helper::getPasteId())->delete();
 
@@ -384,7 +386,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $comment->setNickname($commentData['meta']['nickname']);
         $comment->store();
 
-        $vz = new Vizhash16x16();
+        $vz      = new Vizhash16x16();
         $pngdata = 'data:image/png;base64,' . base64_encode($vz->generate(TrafficLimiter::getHash()));
         $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId())->get();
         $this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set');

+ 161 - 161
tst/PrivateBinTest.php

@@ -1,9 +1,9 @@
 <?php
 
 use PrivateBin\Data\Filesystem;
-use PrivateBin\PrivateBin;
 use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Persistence\TrafficLimiter;
+use PrivateBin\PrivateBin;
 
 class PrivateBinTest extends PHPUnit_Framework_TestCase
 {
@@ -14,7 +14,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function setUp()
     {
         /* Setup Routine */
-        $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);
         $this->reset();
@@ -29,16 +29,16 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
 
     public function reset()
     {
-        $_POST = array();
-        $_GET = array();
+        $_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['purge']['dir'] = $this->_path;
-        $options['traffic']['dir'] = $this->_path;
+        $options                         = parse_ini_file(CONF, true);
+        $options['purge']['dir']         = $this->_path;
+        $options['traffic']['dir']       = $this->_path;
         $options['model_options']['dir'] = $this->_path;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
@@ -72,7 +72,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testViewLanguageSelection()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                              = parse_ini_file(CONF, true);
         $options['main']['languageselection'] = true;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
@@ -94,9 +94,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testViewForceLanguageDefault()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                              = parse_ini_file(CONF, true);
         $options['main']['languageselection'] = false;
-        $options['main']['languagedefault'] = 'fr';
+        $options['main']['languagedefault']   = 'fr';
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
         $_COOKIE['lang'] = 'de';
@@ -118,7 +118,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     {
         $shortener = 'https://shortener.example.com/api?link=';
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                         = parse_ini_file(CONF, true);
         $options['main']['urlshortener'] = $shortener;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
@@ -175,14 +175,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreate()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
+        $_POST                            = Helper::getPaste();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -204,14 +204,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidTimelimit()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste(array('expire' => 25));
+        $_POST                            = Helper::getPaste(array('expire' => 25));
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         TrafficLimiter::canPass();
         ob_start();
         new PrivateBin;
@@ -234,15 +234,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidSize()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                      = parse_ini_file(CONF, true);
         $options['main']['sizelimit'] = 10;
-        $options['traffic']['limit'] = 0;
+        $options['traffic']['limit']  = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
+        $_POST                            = Helper::getPaste();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -258,15 +258,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateProxyHeader()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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';
+        $_POST                            = Helper::getPaste();
+        $_SERVER['HTTP_X_FORWARDED_FOR']  = '::2';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -288,15 +288,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateDuplicateId()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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();
+        $_POST                            = Helper::getPaste();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -312,17 +312,17 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateValidExpire()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
-        $_POST['expire'] = '5min';
-        $_POST['formatter'] = 'foo';
+        $_POST                            = Helper::getPaste();
+        $_POST['expire']                  = '5min';
+        $_POST['formatter']               = 'foo';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
-        $time = time();
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
+        $time                             = time();
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -345,17 +345,17 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateValidExpireWithDiscussion()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
-        $_POST['expire'] = '5min';
-        $_POST['opendiscussion'] = '1';
+        $_POST                            = Helper::getPaste();
+        $_POST['expire']                  = '5min';
+        $_POST['opendiscussion']          = '1';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
-        $time = time();
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
+        $time                             = time();
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -379,15 +379,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidExpire()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPaste();
-        $_POST['expire'] = 'foo';
+        $_POST                            = Helper::getPaste();
+        $_POST['expire']                  = 'foo';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -409,15 +409,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidBurn()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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';
+        $_POST                            = Helper::getPaste();
+        $_POST['burnafterreading']        = 'neither 1 nor 0';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -433,15 +433,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidOpenDiscussion()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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';
+        $_POST                            = Helper::getPaste();
+        $_POST['opendiscussion']          = 'neither 1 nor 0';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -457,15 +457,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateAttachment()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
-        $options['traffic']['limit'] = 0;
+        $options                       = parse_ini_file(CONF, true);
+        $options['traffic']['limit']   = 0;
         $options['main']['fileupload'] = true;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getPasteWithAttachment();
+        $_POST                            = Helper::getPasteWithAttachment();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not exists before posting data');
         ob_start();
         new PrivateBin;
@@ -475,7 +475,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, $response['status'], 'outputs status');
         $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
         $original = json_decode(json_encode($_POST));
-        $stored = $this->_model->read($response['id']);
+        $stored   = $this->_model->read($response['id']);
         foreach (array('data', 'attachment', 'attachmentname') as $key) {
             $this->assertEquals($original->$key, $stored->$key);
         }
@@ -495,16 +495,16 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateBrokenAttachmentUpload()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
-        $options['traffic']['limit'] = 0;
+        $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']);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not exists before posting data');
         ob_start();
         new PrivateBin;
@@ -521,10 +521,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateTooSoon()
     {
         $this->reset();
-        $_POST = Helper::getPaste();
+        $_POST                            = Helper::getPaste();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         ob_end_clean();
@@ -544,15 +544,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateValidNick()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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'];
+        $_POST                            = Helper::getPaste();
+        $_POST['nickname']                = Helper::getComment()['meta']['nickname'];
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -574,17 +574,17 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidNick()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = Helper::getPasteId();
-        $_POST['nickname'] = 'foo';
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = Helper::getPasteId();
+        $_POST['nickname']                = 'foo';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
         ob_start();
         new PrivateBin;
@@ -601,16 +601,16 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateComment()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = Helper::getPasteId();
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
         ob_start();
         new PrivateBin;
@@ -627,16 +627,16 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateInvalidComment()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = 'foo';
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = 'foo';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
         ob_start();
         new PrivateBin;
@@ -653,17 +653,17 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateCommentDiscussionDisabled()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = Helper::getPasteId();
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
-        $paste = Helper::getPaste(array('opendiscussion' => false));
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
+        $paste                            = Helper::getPaste(array('opendiscussion' => false));
         $this->_model->create(Helper::getPasteId(), $paste);
         ob_start();
         new PrivateBin;
@@ -680,16 +680,16 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateCommentInvalidPaste()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = Helper::getPasteId();
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -705,19 +705,19 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testCreateDuplicateComment()
     {
         $this->reset();
-        $options = parse_ini_file(CONF, true);
+        $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());
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists before posting data');
-        $_POST = Helper::getCommentPost();
-        $_POST['pasteid'] = Helper::getPasteId();
-        $_POST['parentid'] = Helper::getPasteId();
+        $_POST                            = Helper::getCommentPost();
+        $_POST['pasteid']                 = Helper::getPasteId();
+        $_POST['parentid']                = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
-        $_SERVER['REMOTE_ADDR'] = '::1';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -739,10 +739,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
-        $this->assertContains(
-            '<div id="cipherdata" class="hidden">' .
-            htmlspecialchars(Helper::getPasteAsJson(), ENT_NOQUOTES) .
-            '</div>',
+        $this->assertRegExp(
+            '#<div id="cipherdata"[^>]*>' .
+            preg_quote(htmlspecialchars(Helper::getPasteAsJson(), ENT_NOQUOTES)) .
+            '</div>#',
             $content,
             'outputs data correctly'
         );
@@ -760,7 +760,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Invalid paste ID\.</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Invalid paste ID\.#s',
             $content,
             'outputs error correctly'
         );
@@ -778,7 +778,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist[^<]*</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
             $content,
             'outputs error correctly'
         );
@@ -798,7 +798,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist[^<]*</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
             $content,
             'outputs error correctly'
         );
@@ -818,10 +818,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         unset($burnPaste['meta']['salt']);
-        $this->assertContains(
-            '<div id="cipherdata" class="hidden">' .
-            htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES) .
-            '</div>',
+        $this->assertRegExp(
+            '#<div id="cipherdata"[^>]*>' .
+            preg_quote(htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)) .
+            '</div>#',
             $content,
             'outputs data correctly'
         );
@@ -835,7 +835,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->reset();
         $paste = Helper::getPaste();
         $this->_model->create(Helper::getPasteId(), $paste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
@@ -859,7 +859,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testReadInvalidJson()
     {
         $this->reset();
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
@@ -876,9 +876,9 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     {
         $this->reset();
         $oldPaste = Helper::getPaste();
-        $meta = array(
+        $meta     = array(
             'syntaxcoloring' => true,
-            'postdate' => $oldPaste['meta']['postdate'],
+            'postdate'       => $oldPaste['meta']['postdate'],
             'opendiscussion' => $oldPaste['meta']['opendiscussion'],
         );
         $oldPaste['meta'] = $meta;
@@ -889,10 +889,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         $meta['formatter'] = 'syntaxhighlighting';
-        $this->assertContains(
-            '<div id="cipherdata" class="hidden">' .
-            htmlspecialchars(Helper::getPasteAsJson($meta), ENT_NOQUOTES) .
-            '</div>',
+        $this->assertRegExp(
+            '#<div id="cipherdata"[^>]*>' .
+            preg_quote(htmlspecialchars(Helper::getPasteAsJson($meta), ENT_NOQUOTES)) .
+            '</div>#',
             $content,
             'outputs data correctly'
         );
@@ -914,10 +914,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         ob_end_clean();
         $oldPaste['meta']['formatter'] = 'plaintext';
         unset($oldPaste['meta']['salt']);
-        $this->assertContains(
-            '<div id="cipherdata" class="hidden">' .
-            htmlspecialchars(Helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES) .
-            '</div>',
+        $this->assertRegExp(
+            '#<div id="cipherdata"[^>]*>' .
+            preg_quote(htmlspecialchars(Helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES)) .
+            '</div>#',
             $content,
             'outputs data correctly'
         );
@@ -931,15 +931,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $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());
-        $_GET['pasteid'] = Helper::getPasteId();
+        $paste               = $this->_model->read(Helper::getPasteId());
+        $_GET['pasteid']     = Helper::getPasteId();
         $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt);
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
+            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted\.#s',
             $content,
             'outputs deleted status correctly'
         );
@@ -953,14 +953,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     {
         $this->reset();
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
-        $_GET['pasteid'] = 'foo';
+        $_GET['pasteid']     = 'foo';
         $_GET['deletetoken'] = 'bar';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Invalid paste ID\.</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Invalid paste ID\.#s',
             $content,
             'outputs delete error correctly'
         );
@@ -973,14 +973,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     public function testDeleteInexistantId()
     {
         $this->reset();
-        $_GET['pasteid'] = Helper::getPasteId();
+        $_GET['pasteid']     = Helper::getPasteId();
         $_GET['deletetoken'] = 'bar';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist[^<]*</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
             $content,
             'outputs delete error correctly'
         );
@@ -993,14 +993,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     {
         $this->reset();
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
-        $_GET['pasteid'] = Helper::getPasteId();
+        $_GET['pasteid']     = Helper::getPasteId();
         $_GET['deletetoken'] = 'bar';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Wrong deletion token[^<]*</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Wrong deletion token\. Paste was not deleted\.#s',
             $content,
             'outputs delete error correctly'
         );
@@ -1016,10 +1016,10 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $burnPaste = Helper::getPaste(array('burnafterreading' => true));
         $this->_model->create(Helper::getPasteId(), $burnPaste);
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_POST['deletetoken'] = 'burnafterreading';
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_POST['deletetoken']             = 'burnafterreading';
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
@@ -1037,17 +1037,17 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->reset();
         $this->_model->create(Helper::getPasteId(), Helper::getPaste());
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_POST['deletetoken'] = 'burnafterreading';
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_POST['deletetoken']             = 'burnafterreading';
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $response = json_decode($content, true);
         $this->assertEquals(1, $response['status'], 'outputs status');
-        $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
+        $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after failing to delete data');
     }
 
     /**
@@ -1060,14 +1060,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not exist before being created');
         $this->_model->create(Helper::getPasteId(), $expiredPaste);
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_GET['pasteid'] = Helper::getPasteId();
+        $_GET['pasteid']     = Helper::getPasteId();
         $_GET['deletetoken'] = 'does not matter in this context, but has to be set';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist[^<]*</div>#',
+            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
             $content,
             'outputs error correctly'
         );
@@ -1084,14 +1084,14 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         unset($paste['meta']['salt']);
         $this->_model->create(Helper::getPasteId(), $paste);
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_GET['pasteid'] = Helper::getPasteId();
+        $_GET['pasteid']     = Helper::getPasteId();
         $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), ServerSalt::get());
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
         $this->assertRegExp(
-            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
+            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted\.#s',
             $content,
             'outputs deleted status correctly'
         );

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio