From a652ab5896b247ea6b211595a257e2126fee57a7 Mon Sep 17 00:00:00 2001
From: rugk <rugk@posteo.de>
Date: Tue, 14 Feb 2017 22:21:55 +0100
Subject: [PATCH] make it work(4): display encrypted pastes

also improved file uploader, better structured
---
 i18n/de.json      |    6 +-
 js/privatebin.js  | 2146 +++++++++++++++++++++++++++------------------
 tpl/bootstrap.php |    8 +-
 3 files changed, 1295 insertions(+), 865 deletions(-)

diff --git a/i18n/de.json b/i18n/de.json
index 87f55cc..bab88a5 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -85,7 +85,7 @@
     "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.",
     "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":
@@ -147,5 +147,7 @@
         "Passwort eingeben",
     "Loading…": "Lädt…",
     "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."
+        "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.",
+    "Nothing to see… Try to enter some text.":
+        "Nichts zu sehen… Versuche etwas Text einzugeben."
 }
diff --git a/js/privatebin.js b/js/privatebin.js
index 8d784cb..118106c 100644
--- a/js/privatebin.js
+++ b/js/privatebin.js
@@ -24,17 +24,23 @@
 // Immediately start random number generator collector.
 sjcl.random.startCollectors();
 
+// 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
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var helper = (function (window, document) {
+    var Helper = (function (window, document) {
         var me = {};
 
         /**
@@ -62,12 +68,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * @private
          * @enum   {string|null}
          */
-        var scriptLocation = null;
+        var baseUri = null;
 
         /**
          * converts a duration (in seconds) into human friendly approximation
          *
-         * @name helper.secondsToHuman
+         * @name Helper.secondsToHuman
          * @function
          * @param  {number} seconds
          * @return {Array}
@@ -104,7 +110,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * text range selection
          *
          * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
-         * @name   helper.selectText
+         * @name   Helper.selectText
          * @function
          * @param  {HTMLElement} element
          */
@@ -113,15 +119,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             var range, selection;
 
             // MS
-            if (document.body.createTextRange)
-            {
+            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 = document.createRange();
                 range.selectNodeContents(element);
@@ -133,15 +135,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set text of a jQuery element (required for IE),
          *
-         * @name   helper.setElementText
+         * @name   Helper.setElementText
          * @function
          * @param  {jQuery} $element - a jQuery element
          * @param  {string} text - the text to enter
          */
         me.setElementText = function($element, text)
         {
-            // @TODO: Can we drop IE 10 support? This function looks crazy and checking oldienotice slows everything down…
-            // I cannot really say, whether this IE10 method is XSS-safe…
             // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
             if ($('#oldienotice').is(':visible')) {
                 var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
@@ -163,21 +163,21 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
          * </pre>
          *
-         * @name   helper.urls2links
+         * @name   Helper.urls2links
          * @function
          * @param  {Object} element - a jQuery DOM element
          */
-        me.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
                 )
@@ -188,7 +188,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * minimal sprintf emulation for %s and %d formats
          *
          * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
-         * @name   helper.sprintf
+         * @name   Helper.sprintf
          * @function
          * @param  {string} format
          * @param  {...*} args - one or multiple parameters injected into format string
@@ -196,11 +196,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          */
         me.sprintf = function()
         {
-            var args = arguments;
-            if (typeof arguments[0] === 'object')
-            {
-                args = arguments[0];
-            }
+            var args = Array.prototype.slice.call(arguments);
             var format = args[0],
                 i = 1;
             return format.replace(/%((%)|s|d)/g, function (m) {
@@ -232,7 +228,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * get value of cookie, if it was set, empty string otherwise
          *
          * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
-         * @name   helper.getCookie
+         * @name   Helper.getCookie
          * @function
          * @param  {string} cname
          * @return {string}
@@ -255,75 +251,38 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * get the current script location (without search or hash part of the URL),
+         * 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.scriptLocation
+         * @name   Helper.baseUri
          * @function
-         * @return {string} current script location
+         * @return {string}
          */
-        me.scriptLocation = function()
+        me.baseUri = function()
         {
             // check for cached version
-            if (scriptLocation !== null) {
-                return scriptLocation;
+            if (baseUri !== null) {
+                return baseUri;
             }
 
-            scriptLocation = window.location.href.substring(
-                    0,
-                    window.location.href.length - window.location.search.length - window.location.hash.length
-                );
+            // get official base uri string, from base tag in head of HTML
+            baseUri = document.baseURI;
 
-            var hashIndex = scriptLocation.indexOf('?');
-
-            if (hashIndex !== -1)
-            {
-                scriptLocation = scriptLocation.substring(0, hashIndex);
+            // if base uri contains query string (when no base tag is present),
+            // it is unwanted
+            if (baseUri.indexOf('?')) {
+                // so we built our own baseuri
+                baseUri = window.location.origin + window.location.pathname;
             }
 
-            return scriptLocation;
-        };
-
-        /**
-         * get the pastes unique identifier from the URL,
-         * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
-         *
-         * @name   helper.pasteId
-         * @function
-         * @return {string} unique identifier
-         */
-        me.pasteId = function()
-        {
-            return window.location.search.substring(1);
-        };
-
-        /**
-         * return the deciphering key stored in anchor part of the URL
-         *
-         * @name   helper.pageKey
-         * @function
-         * @return {string} key
-         */
-        me.pageKey = function()
-        {
-            var key = window.location.hash.substring(1),
-                i = key.indexOf('&');
-
-            // Some web 2.0 services and redirectors add data AFTER the anchor
-            // (such as &utm_source=...). We will strip any additional data.
-            if (i > -1)
-            {
-                key = key.substring(0, i);
-            }
-
-            return key;
+            return baseUri;
         };
 
         /**
          * convert all applicable characters to HTML entities
          *
          * @see    {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
-         * @name   helper.htmlEntities
+         * @name   Helper.htmlEntities
          * @function
          * @param  {string} str
          * @return {string} escaped HTML
@@ -339,15 +298,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     })(window, document);
 
     /**
-     * internationalization methods
+     * internationalization module
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var i18n = (function (window, document) {
+    var I18n = (function (window, document) {
         var me = {};
 
+        /**
+         * const for string of loaded language
+         *
+         * @private
+         * @prop   {string}
+         * @readonly
+         */
+        var languageLoadedEvent = 'languageLoaded';
+
         /**
          * supported languages, minus the built in 'en'
          *
@@ -361,9 +329,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          * built in language
          *
          * @private
-         * @prop   {string}
+         * @prop   {string|null}
          */
-        var language = 'en';
+        var language = null;
 
         /**
          * translation cache
@@ -374,83 +342,125 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         var translations = {};
 
         /**
-         * translate a string, alias for i18n.translate()
+         * translate a string, alias for I18n.translate()
          *
-         * @name   i18n._
+         * for a full description see me.translate
+         *
+         * @name   I18n._
          * @function
+         * @param  {jQuery} $element - optional
          * @param  {string} messageId
          * @param  {...*} args - one or multiple parameters injected into placeholders
          * @return {string}
          */
         me._ = function()
         {
-            return me.translate(arguments);
+            return me.translate.apply(this, arguments);
         };
 
         /**
          * translate a string
          *
-         * @name   i18n.translate
+         * 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}
          */
         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 (!translations.hasOwnProperty(messageId))
-            {
-                if (language !== 'en')
-                {
-                    console.error(
-                        'Missing ' + language + ' translation for: ' + messageId
-                    );
+
+            // if no translation string cannot be found (in translations object)
+            if (!translations.hasOwnProperty(messageId)) {
+                // 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 () {
+                        // re-execute this function
+                        me.translate.apply(this, orgArguments);
+                        // log to show that the previous error could be mitigated
+                        console.log('Fixed missing translation of \'' + messageId + '\' with now loaded language ' + language);
+                    });
+
+                    // and fall back to English for now until the real language
+                    // file is loaded
                 }
+
+                // for all other langauges than English for which thsi behaviour
+                // is expected as it is built-in, log error
+                if (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(translations[messageId]))
-            {
+
+            // lookup plural translation
+            if (usesPlurals && $.isArray(translations[messageId])) {
                 var n = parseInt(args[1] || 1, 10),
                     key = me.getPluralForm(n),
                     maxKey = translations[messageId].length - 1;
-                if (key > maxKey)
-                {
+                if (key > maxKey) {
                     key = maxKey;
                 }
                 args[0] = translations[messageId][key];
                 args[1] = n;
-            }
-            else
-            {
+            } else {
+                // lookup singular translation
                 args[0] = translations[messageId];
             }
-            return helper.sprintf(args);
+
+            // format string
+            var output = Helper.sprintf.apply(this, args);
+
+            // if $element is given, apply text to element
+            if ($element !== null) {
+                $element.text(output);
+            }
+
+            return output;
         };
 
         /**
          * per language functions to use to determine the plural form
          *
          * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
-         * @name   i18n.getPluralForm
+         * @name   I18n.getPluralForm
          * @function
          * @param  {number} n
          * @return {number} array key
@@ -475,37 +485,46 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * load translations into cache, then trigger controller initialization
+         * load translations into cache
          *
-         * @name   i18n.loadTranslations
+         * @name   I18n.loadTranslations
          * @function
          */
         me.loadTranslations = function()
         {
-            var newLanguage = helper.getCookie('lang');
+            var newLanguage = Helper.getCookie('lang');
 
             // auto-select language based on browser settings
-            if (newLanguage.length === 0)
-            {
+            if (newLanguage.length === 0) {
                 newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
             }
 
-            // if language is already used (e.g, default 'en'), skip update
+            // if language is already used skip update
             if (newLanguage === language) {
                 return;
             }
 
+            // if language is built-in (English) skip update
+            if (newLanguage === 'en') {
+                language = 'en';
+                return;
+            }
+
             // 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 strongs from JSON
+            // 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';
             });
         };
 
@@ -517,7 +536,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
      *
      * @class
      */
-    var cryptTool = (function () {
+    var CryptTool = (function () {
         var me = {};
 
         /**
@@ -551,7 +570,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * compress, then encrypt message with given key and password
          *
-         * @name   cryptTool.cipher
+         * @name   CryptTool.cipher
          * @function
          * @param  {string} key
          * @param  {string} password
@@ -577,7 +596,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * decrypt message with key, then decompress
          *
-         * @name   cryptTool.decipher
+         * @name   CryptTool.decipher
          * @function
          * @param  {string} key
          * @param  {string} password
@@ -610,7 +629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * checks whether the crypt tool is ready.
          *
-         * @name   cryptTool.isReady
+         * @name   CryptTool.isReady
          * @function
          * @return {bool}
          */
@@ -622,9 +641,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * checks whether the crypt tool is ready.
          *
-         * @name   cryptTool.isReady
+         * @name   CryptTool.isReady
          * @function
-         * @param {function} - the function to add
+         * @param {function} func
          */
         me.addEntropySeedListener = function(func)
         {
@@ -634,9 +653,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns a random symmetric key
          *
-         * @name   cryptTool.getSymmetricKey
+         * @name   CryptTool.getSymmetricKey
          * @function
-         * @return {string}
+         * @return {string} func
          */
         me.getSymmetricKey = function(func)
         {
@@ -646,16 +665,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * initialize crypt tool
          *
-         * @name   cryptTool.init
+         * @name   CryptTool.init
          * @function
          */
         me.init = function()
         {
             // will fail earlier as sjcl is already passed as a parameter
             // if (typeof sjcl !== 'object') {
-            //     alert.showError(
-            //         i18n._('The library %s is not available.', 'sjcl') +
-            //         i18n._('Messages cannot be decrypted or encrypted.')
+            //     Alert.showError(
+            //         I18n._('The library %s is not available.', 'sjcl') +
+            //         I18n._('Messages cannot be decrypted or encrypted.')
             //     );
             // }
         };
@@ -664,45 +683,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     })();
 
     /**
-     * Data source (aka MVC)
+     * (modal) Data source (aka MVC)
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var modal = (function (window, document) {
+    var Modal = (function (window, document) {
         var me = {};
 
         var $cipherData;
 
-        /**
-         * check if cipher data was supplied
-         *
-         * @name   modal.getCipherData
-         * @function
-         * @return boolean
-         */
-        me.hasCipherData = function()
-        {
-            return (me.getCipherData().length > 0);
-        };
-
-        /**
-         * returns the cipher data
-         *
-         * @name   modal.getCipherData
-         * @function
-         * @return string
-         */
-        me.getCipherData = function()
-        {
-            return $cipherData.text();
-        };
+        var id = null, symmetricKey = null;
 
         /**
          * returns the expiration set in the HTML
          *
-         * @name   modal.getExpirationDefault
+         * @name   Modal.getExpirationDefault
          * @function
          * @return string
          * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
@@ -715,7 +712,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the format set in the HTML
          *
-         * @name   modal.getFormatDefault
+         * @name   Modal.getFormatDefault
          * @function
          * @return string
          * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
@@ -725,12 +722,78 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             return $('#pasteFormatter').val();
         };
 
+        /**
+         * check if cipher data was supplied
+         *
+         * @name   Modal.getCipherData
+         * @function
+         * @return boolean
+         */
+        me.hasCipherData = function()
+        {
+            return (me.getCipherData().length > 0);
+        };
+
+        /**
+         * returns the cipher data
+         *
+         * @name   Modal.getCipherData
+         * @function
+         * @return string
+         */
+        me.getCipherData = function()
+        {
+            return $cipherData.text();
+        };
+
+        /**
+         * get the pastes unique identifier from the URL,
+         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
+         *
+         * @name   Modal.getPasteId
+         * @function
+         * @return {string} unique identifier
+         */
+        me.getPasteId = function()
+        {
+            if (id === null) {
+                id = window.location.search.substring(1);
+            }
+
+            return id;
+        };
+
+        /**
+         * return the deciphering key stored in anchor part of the URL
+         *
+         * @name   Modal.getPasteKey
+         * @function
+         * @return {string} key
+         */
+        me.getPasteKey = function()
+        {
+            if (symmetricKey === null) {
+                symmetricKey = window.location.hash.substring(1);
+
+                // 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)
+                {
+                    symmetricKey = symmetricKey.substring(0, ampersandPos);
+                }
+
+            }
+
+            return symmetricKey;
+        };
+
         /**
          * init navigation manager
          *
          * preloads jQuery elements
          *
-         * @name   modal.init
+         * @name   Modal.init
          * @function
          */
         me.init = function()
@@ -742,39 +805,29 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     })(window, document);
 
     /**
-     * User interface manager
+     * Helper functions for user interface
+     *
+     * everything directly UI-related, which fits nowhere else
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var uiMan = (function (window, document) {
+    var UiHelper = (function (window, document) {
         var me = {};
 
-        // jQuery pre-loaded objects
-        var $clearText,
-            $clonedFile,
-            $comments,
-            $discussion,
-            $image,
-            $prettyMessage,
-            $prettyPrint,
-            $editorTabs,
-            $remainingTime,
-            $replyStatus;
-
         /**
          * handle history (pop) state changes
          *
          * currently this does only handle redirects to the home page.
          *
-         * @name   controller.historyChange
+         * @private
          * @function
          * @param  {Event} event
          */
-        me.historyChange = function(event)
+        function historyChange(event)
         {
-            var currentLocation = helper.scriptLocation();
+            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
@@ -787,62 +840,50 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * reload the page
          *
-         * This takes the user to the PrivateBin home page.
+         * This takes the user to the PrivateBin homepage.
          *
-         * @name   controller.reloadPage
+         * @name   UiHelper.reloadHome
          * @function
-         * @param  {Event} event
          */
-        me.reloadPage = function(event)
+        me.reloadHome = function()
         {
-            window.location.href = helper.scriptLocation();
-            event.preventDefault();
+            window.location.href = Helper.baseUri();
         };
 
         /**
-         * main UI manager
+         * initialize
          *
-         * @name   controller.init
+         * @name   UiHelper.init
          * @function
          */
         me.init = function()
         {
-            // hide "no javascript" message
-            $('#noscript').hide();
+            // update link to home page
+            $('.reloadlink').prop('href', Helper.baseUri());
 
-            // bind events
-            $('.reloadlink').click(me.reloadPage);
-
-            $(window).on('popstate', me.historyChange);
+            $(window).on('popstate', historyChange);
         };
 
         return me;
     })(window, document);
 
     /**
-     * alert/notification manager
+     * Alert/error manager
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var alert = (function (window, document) {
+    var Alert = (function (window, document) {
         var me = {};
 
-        var $attachment,
-            $attachmentLink,
-            $errorMessage,
-            $clonedFile,
-            $fileWrap,
-            $status,
-            $pasteSuccess,
-            $shortenButton,
-            $pasteUrl;
+        var $errorMessage,
+            $status;
 
         /**
          * display a status message
          *
-         * @name   controller.showStatus
+         * @name   Alert.showStatus
          * @function
          * @param  {string} message - text to display
          * @param  {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
@@ -853,102 +894,83 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             $status.text(message);
         };
 
-        /**
-         * display a status message for replying to comments
-         *
-         * @name   controller.showStatus
-         * @function
-         * @param  {string} message - text to display
-         * @param  {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
-         */
-        me.showReplyStatus = function(message, spin)
-        {
-            if (spin || false) {
-                $replyalert.find('.spinner').removeClass('hidden')
-            }
-            $replyalert.text(message);
-        };
-
         /**
          * hides any status messages
          *
-         * @name   controller.hideMessages
+         * @name   Alert.hideMessages
          * @function
          */
         me.hideMessages = function()
         {
             $status.html(' ');
+            $errorMessage.addClass('hidden');
         };
 
         /**
          * display an error message
          *
-         * @name   alert.showError
+         * @name   Alert.showError
          * @function
          * @param  {string} message - text to display
          */
         me.showError = function(message)
         {
+            console.error('Error shown: ' + message);
+
             $errorMessage.removeClass('hidden');
-            $errorMessage.find(':last').text(message);
+            $errorMessage.find(':last').text(' ' + message);
         };
 
         /**
-         * display an error message
+         * init status manager
          *
-         * @name   alert.showError
-         * @function
-         * @param  {string} message - text to display
-         */
-        me.showReplyError = function(message)
-        {
-            $replyalert.addClass('alert-danger');
-            $replyalert.addClass($errorMessage.attr('class')); // @TODO ????
-
-            $replyalert.text(message);
-        };
-
-        /**
-         * removes the existing attachment
+         * preloads jQuery elements
          *
-         * @name   alert.removeAttachment
+         * @name   Alert.init
          * @function
          */
-        me.removeAttachment = function()
+        me.init = function()
         {
-            $clonedFile.addClass('hidden');
-            // removes the saved decrypted file data
-            $attachmentLink.attr('href', '');
-            // the only way to deselect the file is to recreate the input // @TODO really?
-            $fileWrap.html($fileWrap.html());
-            $fileWrap.removeClass('hidden');
+            // hide "no javascript" message
+            $('#noscript').hide();
+
+            $errorMessage = $('#errormessage');
+            $status = $('#status');
+
+            // display status returned by php code, if any (eg. paste was properly deleted)
+            // @TODO remove this by handling errors in a different way
+            if ($status.text().length > 0)
+            {
+                me.showStatus($status.text());
+                return;
+            }
+
+            // keep line height even if content empty
+            $status.html(' '); // @TODO what? remove?
+
+            // display error message from php code
+            if ($errorMessage.text().length > 1) {
+                Alert.showError($errorMessage.text());
+            }
         };
 
-        /**
-         * checks if there is an attachment
-         *
-         * @name   alert.hasAttachment
-         * @function
-         */
-        me.hasAttachment = function()
-        {
-            return typeof $attachmentLink.attr('href') !== 'undefined'
-        };
+        return me;
+    })(window, document);
 
-        /**
-         * return the attachment
-         *
-         * @name   alert.getAttachment
-         * @function
-         * @returns {array}
-         */
-        me.getAttachment = function()
-        {
-            return [
-                $attachmentLink.attr('href'),
-                $attachmentLink.attr('download')
-            ];
-        };
+    /**
+     * handles paste status/result
+     *
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var PasteStatus = (function (window, document) {
+        var me = {};
+
+        var $pasteSuccess,
+            $shortenButton,
+            $pasteUrl,
+            $remainingTime;
 
         /**
          * forward to URL shortener
@@ -963,45 +985,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                                    + encodeURIComponent($pasteUrl.attr('href'));
         }
 
-        /**
-         * reload the page
-         *
-         * This takes the user to the PrivateBin home page.
-         *
-         * @name   controller.createPasteNotification
-         * @function
-         * @param  {string} url
-         * @param  {string} deleteUrl
-         */
-        me.createPasteNotification = function(url, deleteUrl)
-        {
-            $('#pastelink').find(':first').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]);
-        };
-
         /**
          * 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   controller.pasteLinkClick
+         * @name   Controller.pasteLinkClick
          * @function
          * @param  {Event} event
          */
@@ -1014,127 +1004,196 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             }
         }
 
+        /**
+         * creates a notification after a successfull paste upload
+         *
+         * @name   PasteStatus.createPasteNotification
+         * @function
+         * @param  {string} url
+         * @param  {string} deleteUrl
+         */
+        me.createPasteNotification = function(url, deleteUrl)
+        {
+            $('#pastelink').find(':first').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]);
+        };
+
+        /**
+         * shows the remaining time
+         *
+         * @function
+         * @param {object} pasteMetaData
+         */
+        me.showRemainingTime = function(pasteMetaData)
+        {
+            if (pasteMetaData.burnafterreading) {
+                // display paste "for your eyes only" if it is deleted
+
+                // actually remove paste, before we claim it is deleted
+                Controller.removePaste(Modal.getPasteId(), 'burnafterreading');
+
+                I18n._($remainingTime.find(':last'), "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.'
+                    ];
+
+                I18n._($remainingTime.find(':last'), expirationLabel, expiration[0]);
+                $remainingTime.removeClass('foryoureyesonly')
+            } else {
+                // never expires
+                return;
+            }
+
+            // in the end, display notification
+            $remainingTime.removeClass('hidden');
+        };
+
         /**
          * init status manager
          *
          * preloads jQuery elements
          *
-         * @name   alert.init
+         * @name   Alert.init
          * @function
          */
         me.init = function()
         {
-            // hide "no javascript" message
-            $('#noscript').hide();
-
             $shortenButton = $('#shortenbutton');
-            $attachment = $('#attachment');
-            $attachmentLink = $('#attachment a');
-            $clonedFile = $('#clonedfile');
-            $errorMessage = $('#errormessage');
-            $fileWrap = $('#filewrap');
             $pasteSuccess = $('#pasteSuccess');
-            // $pasteUrl is saved in submitPasteUpload() if/after it is
-            // actually created
-            $status = $('#status');
-            // @TODO $replyStatus …
+            // $pasteUrl is saved in me.createPasteNotification() after creation
+            $remainingTime = $('#remainingtime');
 
             // bind elements
             $shortenButton.click(sendToShortener);
-
-            // display status returned by php code, if any (eg. paste was properly deleted)
-            // @TODO remove this by handling errors in a different way
-            if ($status.text().length > 0)
-            {
-                me.showStatus($status.text());
-                return;
-            }
-
-            // keep line height even if content empty
-            $status.html(' '); // @TODO what? remove?
-
-            // display error message from php code
-            if ($errorMessage.text().length > 1) {
-                me.showError($errorMessage.text());
-            }
         };
 
         return me;
     })(window, document);
 
     /**
-     * Passwort prompt
+     * password prompt
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var prompt = (function (window, document) {
+    var Prompt = (function (window, document) {
         var me = {};
 
         var $passwordModal,
             $passwordForm,
             $passwordDecrypt;
 
+        var password = '',
+            passwordCallback = null;
+
         /**
          * ask the user for the password and set it
          *
-         * @name   controller.requestPassword
+         * the callback set via setPasswordCallback is executed
+         *
+         * @name Prompt.requestPassword()
          * @function
          */
         me.requestPassword = function()
         {
-            if ($passwordModal.length === 0) {
-                // old method for page template
-                var password = prompt(i18n._('Please enter the password for this paste:'), '');
-                if (password === null)
-                {
-                    // @TODO when does this happen?
-                    throw 'password prompt canceled';
-                }
-                if (password.length === 0)
-                {
-                    // recursive…
-                    me.requestPassword();
-                } else {
-                    $passwordInput.val(password);
-                    me.displayMessages();
-                }
-            } else {
-                // new bootstrap method
-                $passwordModal.modal();
-            }
+            // show new bootstrap method
+            $passwordModal.modal({
+                backdrop: 'static',
+                keyboard: false
+            });
         };
 
         /**
-         * decrypt using the password from the modal dialog
+         * get cached password or password from easy Prompt
          *
-         * @name   controller.decryptPasswordModal
+         * If you do not get a password with this function, use
+         * requestPassword
+         *
+         * @name   Prompt.getPassword
          * @function
          */
         me.getPassword = function()
         {
-            if ($passwordDecrypt.val().length === 0) {
-                me.requestPassword();
+            if (password.length !== 0) {
+                return password;
             }
 
-            return $passwordDecrypt.val();
-            // $passwordInput.val($passwordDecrypt.val());
-            // me.displayMessages();
+            if ($passwordModal.length === 0) {
+                // 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…
+                    me.getPassword();
+                } else {
+                    password = newPassword;
+                }
+            }
+
+            return password;
         };
 
         /**
-         * submit a password in the modal dialog
+         * setsthe callback called when password is entered
          *
-         * @name   controller.submitPasswordModal
+         * @name   Prompt.setPasswordCallback
+         * @function
+         * @param {functions} setPasswordCallback
+         */
+        me.setPasswordCallback = function(callback)
+        {
+            passwordCallback = callback;
+        };
+
+        /**
+         * submit a password in the Modal dialog
+         *
+         * @private
          * @function
          * @param  {Event} event
          */
-        me.submitPasswordModal = function(event)
+        function submitPasswordModal(event)
         {
-            event.preventDefault();
+            // get input
+            password = $passwordDecrypt.val();
+
+            // hide modal
             $passwordModal.modal('hide');
-        };
+
+            if (passwordCallback !== null) {
+                passwordCallback();
+            }
+
+            event.preventDefault();
+        }
 
 
         /**
@@ -1142,7 +1201,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * preloads jQuery elements
          *
-         * @name   controller.init
+         * @name   Controller.init
          * @function
          */
         me.init = function()
@@ -1154,25 +1213,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             // bind events
 
             // focus password input when it is shown
-            $passwordModal.on('shown.bs.modal', function () {
+            $passwordModal.on('shown.bs.Modal', function () {
                 $passwordDecrypt.focus();
             });
-            // handle modal password request on decryption
-            $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal);
-            $passwordForm.submit(me.submitPasswordModal);
+            // handle Modal password submission
+            $passwordForm.submit(submitPasswordModal);
         };
 
         return me;
     })(window, document);
 
     /**
-     * Manage paste/message input
+     * Manage paste/message input, and preview tab
+     *
+     * Note that the actual preview is handled by PasteViewer.
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var editor = (function (window, document) {
+    var Editor = (function (window, document) {
         var me = {};
 
         var $message,
@@ -1185,7 +1245,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * support input of tab character
          *
-         * @name   editor.supportTabs
+         * @name   Editor.supportTabs
          * @function
          * @param  {Event} event
          * @TODO doc what is @this here?
@@ -1211,9 +1271,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         /**
-         * view the editor tab
+         * view the Editor tab
          *
-         * @name   editor.viewEditor
+         * @name   Editor.viewEditor
          * @function
          * @param  {Event} event - optional
          */
@@ -1223,25 +1283,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             $messageEdit.addClass('active');
             $messagePreview.removeClass('active');
 
-            pasteViewer.hide();
+            PasteViewer.hide();
 
             // reshow input
             $message.removeClass('hidden');
 
             me.focusInput();
-            // me.stateNewPaste();
 
             // finish
             isPreview = false;
-            // if (typeof event === 'undefined') {
-            //     event.preventDefault();
-            // } // @TODO confirm this is not needed
+
+            // prevent jumping of page to top
+            if (typeof event !== 'undefined') {
+                event.preventDefault();
+            }
         }
 
         /**
          * view the preview tab
          *
-         * @name   editor.viewPreview
+         * @name   Editor.viewPreview
          * @function
          * @param  {Event} event
          */
@@ -1256,20 +1317,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
 
             // show preview
             $('#errormessage').find(':last')
-            pasteViewer.setText($message.val());
-            pasteViewer.trigger();
+            PasteViewer.setText($message.val());
+            PasteViewer.run();
 
             // finish
             isPreview = true;
-            // if (typeof event === 'undefined') {
-            //     event.preventDefault();
-            // } // @TODO confirm this is not needed
+
+            // prevent jumping of page to top
+            if (typeof event !== 'undefined') {
+                event.preventDefault();
+            }
         }
 
         /**
          * get the state of the preview
          *
-         * @name   editor.isPreview
+         * @name   Editor.isPreview
          * @function
          */
         me.isPreview = function()
@@ -1278,9 +1341,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         /**
-         * reset the editor view
+         * reset the Editor view
          *
-         * @name   editor.resetInput
+         * @name   Editor.resetInput
          * @function
          */
         me.resetInput = function()
@@ -1295,9 +1358,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * shows the editor
+         * shows the Editor
          *
-         * @name   editor.show
+         * @name   Editor.show
          * @function
          */
         me.show = function()
@@ -1307,9 +1370,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * hides the editor
+         * hides the Editor
          *
-         * @name   editor.reset
+         * @name   Editor.reset
          * @function
          */
         me.hide = function()
@@ -1321,7 +1384,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * focuses the message input
          *
-         * @name   editor.focusInput
+         * @name   Editor.focusInput
          * @function
          */
         me.focusInput = function()
@@ -1332,7 +1395,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the current text
          *
-         * @name   editor.getText
+         * @name   Editor.getText
          * @function
          * @return {string}
          */
@@ -1346,7 +1409,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * preloads jQuery elements
          *
-         * @name   editor.init
+         * @name   Editor.init
          * @function
          */
         me.init = function()
@@ -1367,23 +1430,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     })(window, document);
 
     /**
-     * Parse and show paste.
+     * (view) Parse and show paste.
      *
      * @param  {object} window
      * @param  {object} document
      * @class
      */
-    var pasteViewer = (function (window, document) {
+    var PasteViewer = (function (window, document) {
         var me = {};
 
-        var $clearText,
-            $comments,
-            $discussion,
-            $image,
+        var $clonedFile,
+            $plainText,
             $placeholder,
             $prettyMessage,
-            $prettyPrint,
-            $remainingTime;
+            $prettyPrint;
 
         var text,
             format = 'plaintext',
@@ -1393,7 +1453,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * apply the set format on paste and displays it
          *
-         * @name   pasteViewer.parsePaste
          * @private
          * @function
          */
@@ -1405,8 +1464,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             }
 
             // set text
-            helper.setElementText($clearText, text);
-            helper.setElementText($prettyPrint, text);
+            Helper.setElementText($plainText, text);
+            Helper.setElementText($prettyPrint, text);
 
             switch (format) {
                 case 'markdown':
@@ -1415,11 +1474,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                         tables: true,
                         tablesHeaderId: true
                     });
-                    $clearText.html(
+                    $plainText.html(
                         converter.makeHtml(text)
                     );
                     // add table classes from bootstrap css
-                    $clearText.find('table').addClass('table-condensed table-bordered');
+                    $plainText.find('table').addClass('table-condensed table-bordered');
                     break;
                 case 'syntaxhighlighting':
                     // @TODO is this really needed or is "one" enough?
@@ -1430,14 +1489,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
 
                     $prettyPrint.html(
                         prettyPrintOne(
-                            helper.htmlEntities(text), null, true
+                            Helper.htmlEntities(text), null, true
                         )
                     );
                     // fall through, as the rest is the same
                 default: // = 'plaintext'
                     // convert URLs to clickable links
-                    helper.urls2links($clearText);
-                    helper.urls2links($prettyPrint);
+                    Helper.urls2links($plainText);
+                    Helper.urls2links($prettyPrint);
 
                     $prettyPrint.css('white-space', 'pre-wrap');
                     $prettyPrint.css('word-break', 'normal');
@@ -1448,7 +1507,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * displays the paste
          *
-         * @name   pasteViewer.show
          * @private
          * @function
          */
@@ -1464,234 +1522,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
 
             switch (format) {
                 case 'markdown':
-                    $clearText.removeClass('hidden');
+                    $plainText.removeClass('hidden');
                     $prettyMessage.addClass('hidden');
                     break;
                 default:
-                    $clearText.addClass('hidden');
+                    $plainText.addClass('hidden');
                     $prettyMessage.removeClass('hidden');
                     break;
             }
         }
 
-        /**
-         * show decrypted text in the display area, including discussion (if open)
-         *
-         * @name   pasteViewer.displayPaste
-         * @function
-         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
-         */
-        me.displayPaste = function(paste)
-        {
-            paste = paste || $.parseJSON(modal.getCipherData());
-            var key = helper.pageKey(),
-                password = $passwordInput.val();
-            if (!$prettyPrint.hasClass('prettyprinted')) {
-                // Try to decrypt the paste.
-                try
-                {
-                    if (paste.attachment)
-                    {
-                        var attachment = cryptTool.decipher(key, password, paste.attachment);
-                        if (attachment.length === 0)
-                        {
-                            if (password.length === 0)
-                            {
-                                me.requestPassword();
-                                return;
-                            }
-                            attachment = cryptTool.decipher(key, password, paste.attachment);
-                        }
-                        if (attachment.length === 0)
-                        {
-                            throw 'failed to decipher attachment';
-                        }
-
-                        if (paste.attachmentname)
-                        {
-                            var attachmentname = cryptTool.decipher(key, password, paste.attachmentname);
-                            if (attachmentname.length > 0)
-                            {
-                                $attachmentLink.attr('download', attachmentname);
-                            }
-                        }
-                        $attachmentLink.attr('href', attachment);
-                        $attachment.removeClass('hidden');
-
-                        // if the attachment is an image, display it
-                        var imagePrefix = 'data:image/';
-                        if (attachment.substring(0, imagePrefix.length) === imagePrefix)
-                        {
-                            $image.html(
-                                $(document.createElement('img'))
-                                    .attr('src', attachment)
-                                    .attr('class', 'img-thumbnail')
-                            );
-                            $image.removeClass('hidden');
-                        }
-                    }
-                    var cleartext = cryptTool.decipher(key, password, paste.data);
-                    if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
-                    {
-                        me.requestPassword();
-                        return;
-                    }
-                    if (cleartext.length === 0 && !paste.attachment)
-                    {
-                        throw 'failed to decipher message';
-                    }
-
-                    $passwordInput.val(password);
-                    if (cleartext.length > 0)
-                    {
-                        pasteViewer.setFormat(paste.meta.formatter);
-                        me.formatPaste(paste.meta.formatter, cleartext);
-                    }
-                }
-                catch(err)
-                {
-                    me.stateOnlyNewPaste();
-                    me.showError(i18n._('Could not decrypt data (Wrong key?)'));
-                    return;
-                }
-            }
-
-            // 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.'
-                    ];
-                $remainingTime.find(':last').text(i18n._(expirationLabel, expiration[0]));
-                $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: helper.scriptLocation() + '?' + helper.pasteId(),
-                    data: {deletetoken: 'burnafterreading'},
-                    dataType: 'json',
-                    headers: headers
-                })
-                .fail(function() {
-                    controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
-                });
-                $remainingTime.find(':last').text(i18n._(
-                    'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
-                ));
-                $remainingTime.addClass('foryoureyesonly')
-                                  .removeClass('hidden');
-                // discourage cloning (as it can't really be prevented)
-                $cloneButton.addClass('hidden');
-            }
-
-            // if the discussion is opened on this paste, display it
-            if (paste.meta.opendiscussion)
-            {
-                $comments.html('');
-
-                var $divComment;
-
-                // iterate over comments
-                for (var i = 0; i < paste.comments.length; ++i)
-                {
-                    var $place = $comments,
-                        comment = paste.comments[i],
-                        commentText = cryptTool.decipher(key, password, comment.data),
-                        $parentComment = $('#comment_' + comment.parentid);
-
-                    $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>');
-                    var $divCommentData = $divComment.find('div.commentdata');
-
-                    // if parent comment exists
-                    if ($parentComment.length)
-                    {
-                        // shift comment to the right
-                        $place = $parentComment;
-                    }
-                    $divComment.find('button').click({commentid: comment.id}, me.openReply);
-                    helper.setElementText($divCommentData, commentText);
-                    helper.urls2links($divCommentData);
-
-                    // try to get optional nickname
-                    var nick = cryptTool.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);
-                }
-
-                // add 'add new comment' area
-                $divComment = $(
-                    '<div class="comment"><button class="btn btn-default btn-sm">' +
-                    i18n._('Add comment') + '</button></div>'
-                );
-                $divComment.find('button').click({commentid: helper.pasteId()}, me.openReply);
-                $comments.append($divComment);
-                $discussion.removeClass('hidden');
-            }
-        };
-
-        /**
-         * open the comment entry when clicking the "Reply" button of a comment
-         *
-         * @name   pasteViewer.openReply
-         * @function
-         * @param  {Event} event
-         */
-        me.openReply = function(event)
-        {
-            event.preventDefault();
-
-            // remove any other reply area
-            $('div.reply').remove();
-
-            var source = $(event.target),
-                commentid = event.data.commentid,
-                hint = i18n._('Optional nickname...'),
-                $reply = $('#replytemplate');
-            $reply.find('button').click(
-                {parentid: commentid},
-                me.sendComment
-            );
-            source.after($reply);
-            $replyStatus = $('#replystatus'); // when ID --> put into HTML
-            $('#replymessage').focus();
-        };
-
         /**
          * sets the format in which the text is shown
          *
-         * @name   pasteViewer.setFormat
+         * @name   PasteViewer.setFormat
          * @function
          * @param {string}  the the new format
          */
@@ -1706,7 +1550,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the current format
          *
-         * @name   pasteViewer.setFormat
+         * @name   PasteViewer.setFormat
          * @function
          * @return {string}
          */
@@ -1715,12 +1559,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             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   editor.init
+         * @name   Editor.init
          * @function
-         * @param {string}  the text to show
+         * @param {string} newText the text to show
          */
         me.setText = function(newText)
         {
@@ -1733,10 +1589,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * show/update the parsed text (preview)
          *
-         * @name   pasteViewer.trigger
+         * @name   PasteViewer.run
          * @function
          */
-        me.trigger = function()
+        me.run = function()
         {
             if (isChanged) {
                 parsePaste();
@@ -1752,16 +1608,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * hide parsed text (preview)
          *
-         * @name   pasteViewer.hide
+         * @name   PasteViewer.hide
          * @function
          */
         me.hide = function()
         {
             if (!isDisplayed) {
-                console.warn('pasteViewer was called to hide the parsed view, but it is already hidden.');
+                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
             }
 
-            $clearText.addClass('hidden');
+            $plainText.addClass('hidden');
             $prettyMessage.addClass('hidden');
             $placeholder.addClass('hidden');
 
@@ -1773,36 +1629,253 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * preloads jQuery elements
          *
-         * @name   editor.init
+         * @name   Editor.init
          * @function
          */
         me.init = function()
         {
-            $clearText = $('#cleartext');
-            $comments = $('#comments');
-            $discussion = $('#discussion');
-            $image = $('#image');
+            $plainText = $('#plaintext');
             $placeholder = $('#placeholder');
             $prettyMessage = $('#prettymessage');
             $prettyPrint = $('#prettyprint');
-            $remainingTime = $('#remainingtime');
 
             // check requirements
             if (typeof prettyPrintOne !== 'function') {
-                alert.showError(
-                    i18n._('The library %s is not available.', 'pretty print') +
-                    i18n._('This may cause display errors.')
+                Alert.showError(
+                    I18n._('The library %s is not available.', 'pretty print') +
+                    I18n._('This may cause display errors.')
                 );
             }
             if (typeof showdown !== 'object') {
-                alert.showError(
-                    i18n._('The library %s is not available.', 'showdown') +
-                    i18n._('This may cause display errors.')
+                Alert.showError(
+                    I18n._('The library %s is not available.', 'showdown') +
+                    I18n._('This may cause display errors.')
                 );
             }
 
             // get default option from template/HTML or fall back to set value
-            format = modal.getFormatDefault() || format;
+            format = Modal.getFormatDefault() || format;
+        };
+
+        return me;
+    })(window, document);
+
+    /**
+     * (view) Show attachment and preview if possible
+     *
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var AttachmentViewer = (function (window, document) {
+        var me = {};
+
+        var $attachment,
+            $attachmentLink,
+            $clonedFile,
+            $attachmentPreview,
+            $fileWrap;
+
+        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 existing attachment
+         *
+         * @name   AttachmentViewer.removeAttachment
+         * @function
+         */
+        me.removeAttachment = function()
+        {
+             // (new)
+            $attachment.addClass('hidden');
+            $attachmentPreview.addClass('hidden');
+
+            $clonedFile.addClass('hidden');
+            // removes the saved decrypted file data
+            $attachmentLink.attr('href', '');
+            // the only way to deselect the file is to recreate the input // @TODO really?
+            $fileWrap.html($fileWrap.html());
+            $fileWrap.removeClass('hidden');
+
+            // reset internal variables
+        };
+
+        /**
+         * checks if there is an attachment
+         *
+         * @name   AttachmentViewer.hasAttachment
+         * @function
+         */
+        me.hasAttachment = function()
+        {
+            return typeof $attachmentLink.attr('href') !== 'undefined'
+        };
+
+        /**
+         * return the attachment
+         *
+         * @name   AttachmentViewer.getAttachment
+         * @function
+         * @returns {array}
+         */
+        me.getAttachment = function()
+        {
+            return [
+                $attachmentLink.attr('href'),
+                $attachmentLink.attr('download')
+            ];
+        };
+
+        /**
+         * initiate
+         *
+         * preloads jQuery elements
+         *
+         * @name   AttachmentViewer.init
+         * @function
+         */
+        me.init = function()
+        {
+            $attachmentPreview = $('#attachmentPreview');
+            $attachment = $('#attachment');
+            $attachmentLink = $('#attachment a');
+            $clonedFile = $('#clonedfile');
+            $fileWrap = $('#filewrap');
+        };
+
+        return me;
+    })(window, document);
+
+    /**
+     * (view) Shows discussion thread and handles replies
+     *
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var DiscussionViewer = (function (window, document) {
+        var me = {};
+
+        var $comments,
+            $discussion;
+
+        /**
+         * display a status message for replying to comments
+         *
+         * @name   Controller.showStatus
+         * @function
+         * @param  {string} message - text to display
+         * @param  {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
+         */
+        me.showReplyStatus = function(message, spin)
+        {
+            if (spin || false) {
+                $replyalert.find('.spinner').removeClass('hidden')
+            }
+            $replyalert.text(message);
+        };
+
+        /**
+         * display an error message
+         *
+         * @name   Alert.showError
+         * @function
+         * @param  {string} message - text to display
+         */
+        me.showReplyError = function(message)
+        {
+            $replyalert.addClass('Alert-danger');
+            $replyalert.addClass($errorMessage.attr('class')); // @TODO ????
+
+            $replyalert.text(message);
+        };
+
+        /**
+         * open the comment entry when clicking the "Reply" button of a comment
+         *
+         * @name   PasteViewer.openReply
+         * @function
+         * @param  {Event} event
+         */
+        me.openReply = function(event)
+        {
+            event.preventDefault();
+
+            // remove any other reply area
+            $('div.reply').remove();
+
+            var source = $(event.target),
+                commentid = event.data.commentid,
+                hint = I18n._('Optional nickname...'),
+                $reply = $('#replytemplate');
+            $reply.find('button').click(
+                {parentid: commentid},
+                me.sendComment
+            );
+            source.after($reply);
+            $replyStatus = $('#replystatus'); // when ID --> put into HTML
+            $('#replymessage').focus();
+        };
+
+        /**
+         * initiate
+         *
+         * preloads jQuery elements
+         *
+         * @name   AttachmentViewer.init
+         * @function
+         */
+        me.init = function()
+        {
+            $comments = $('#comments');
+            $discussion = $('#discussion');
+            // $replyStatus in openReply()
         };
 
         return me;
@@ -1813,10 +1886,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
      *
      * @param  {object} window
      * @param  {object} document
-     * @name state
      * @class
      */
-    var topNav = (function (window, document) {
+    var TopNav = (function (window, document) {
         var me = {};
 
         var createButtonsDisplayed = false;
@@ -1843,7 +1915,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set the expiration on bootstrap templates in dropdown
          *
-         * @name   topNav.updateExpiration
+         * @name   TopNav.updateExpiration
          * @function
          * @param  {Event} event
          */
@@ -1862,7 +1934,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set the format on bootstrap templates in dropdown
          *
-         * @name   topNav.updateFormat
+         * @name   TopNav.updateFormat
          * @function
          * @param  {Event} event
          */
@@ -1874,11 +1946,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             // update dropdown display and save new format
             var newFormat = $target.data('format');
             $('#pasteFormatterDisplay').text($target.text());
-            pasteViewer.setFormat(newFormat);
+            PasteViewer.setFormat(newFormat);
 
             // update preview
-            if (editor.isPreview()) {
-                pasteViewer.trigger();
+            if (Editor.isPreview()) {
+                PasteViewer.run();
             }
 
             event.preventDefault();
@@ -1887,7 +1959,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * when "burn after reading" is checked, disable discussion
          *
-         * @name   topNav.changeBurnAfterReading
+         * @name   TopNav.changeBurnAfterReading
          * @function
          */
         function changeBurnAfterReading()
@@ -1906,7 +1978,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * when discussion is checked, disable "burn after reading"
          *
-         * @name   topNav.changeOpenDiscussion
+         * @name   TopNav.changeOpenDiscussion
          * @function
          */
         function changeOpenDiscussion()
@@ -1925,44 +1997,44 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * return raw text
          *
-         * @name   topNav.rawText
+         * @name   TopNav.rawText
          * @function
          * @param  {Event} event
          */
         function rawText(event)
         {
-            var paste = pasteViewer.getFormat() === 'markdown' ?
-                $prettyPrint.text() : $clearText.text();
+            var paste = PasteViewer.getFormat() === 'markdown' ?
+                $prettyPrint.text() : $plainText.text();
             history.pushState(
-                null, document.title, helper.scriptLocation() + '?' +
-                helper.pasteId() + '#' + helper.pageKey()
+                null, document.title, Helper.baseUri() + '?' +
+                Modal.getPasteId() + '#' + Modal.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 newDoc = document.open('text/html', 'replace');
-            newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
+            newDoc.write('<pre>' + Helper.htmlEntities(paste) + '</pre>');
             newDoc.close();
 
             event.preventDefault();
         }
 
         /**
-         * set the language in a cookie and reload the page
+         * saves the language in a cookie and reloads the page
          *
-         * @name   topNav.setLanguage
+         * @name   TopNav.setLanguage
          * @function
          * @param  {Event} event
          */
         function setLanguage(event)
         {
             document.cookie = 'lang=' + $(event.target).data('lang');
-            me.reloadPage(event);
+            UiHelper.reloadHome();
         }
 
         /**
          * Shows all elements belonging to viwing an existing pastes
          *
-         * @name   topNav.hideAllElem
+         * @name   TopNav.hideAllElem
          * @function
          */
         me.showViewButtons = function()
@@ -1981,7 +2053,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * Hides all elements belonging to existing pastes
          *
-         * @name   topNav.hideAllElem
+         * @name   TopNav.hideAllElem
          * @function
          */
         me.hideViewButtons = function()
@@ -1991,6 +2063,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                 return;
             }
 
+            $newButton.removeClass('hidden');
             $cloneButton.addClass('hidden');
             $rawTextButton.addClass('hidden');
 
@@ -2000,7 +2073,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * shows all elements needed when creating a new paste
          *
-         * @name   topNav.setLanguage
+         * @name   TopNav.setLanguage
          * @function
          */
         me.showCreateButtons = function()
@@ -2026,7 +2099,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * shows all elements needed when creating a new paste
          *
-         * @name   topNav.setLanguage
+         * @name   TopNav.setLanguage
          * @function
          */
         me.hideCreateButtons = function()
@@ -2036,12 +2109,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                 return;
             }
 
+            $newButton.addClass('hidden');
             $sendButton.addClass('hidden');
             $expiration.addClass('hidden');
             $formatter.addClass('hidden');
             $burnAfterReadingOption.addClass('hidden');
             $openDiscussionOption.addClass('hidden');
-            $newButton.addClass('hidden');
             $password.addClass('hidden');
             $attach.addClass('hidden');
             // $clonedFile.addClass('hidden'); // @TODO
@@ -2052,18 +2125,40 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * only shows the "new paste" button
          *
-         * @name   topNav.setLanguage
+         * @name   TopNav.setLanguage
          * @function
          */
         me.showNewPasteButton = function()
         {
-            $newButton.addClass('hidden');
+            $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');
         };
 
         /**
          * shows a loading message, optionally with a percentage
          *
-         * @name   topNav.showLoading
+         * @name   TopNav.showLoading
          * @function
          * @param  {string} message optional, default: 'Loading…'
          * @param  {int}    percentage optional, default: null
@@ -2072,10 +2167,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         {
             // default message text
             if (typeof message === 'undefined') {
-                message = i18n._('Loading…');
+                message = I18n._('Loading…');
             }
 
-            console.log($loadingIndicator);
             // currently percentage parameter is ignored
             if (message !== null) {
                 $loadingIndicator.find(':last').text(message);
@@ -2086,7 +2180,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * hides the loading message
          *
-         * @name   topNav.hideLoading
+         * @name   TopNav.hideLoading
          * @function
          */
         me.hideLoading = function()
@@ -2097,7 +2191,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * collapses the navigation bar if nedded
          *
-         * @name   topNav.collapseBar
+         * @name   TopNav.collapseBar
          * @function
          */
         me.collapseBar = function()
@@ -2114,7 +2208,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the currently set expiration time
          *
-         * @name   topNav.getExpiration
+         * @name   TopNav.getExpiration
          * @function
          * @return {int}
          */
@@ -2126,7 +2220,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the currently selected file(s)
          *
-         * @name   topNav.getFileList
+         * @name   TopNav.getFileList
          * @function
          * @return {FileList|null}
          */
@@ -2149,7 +2243,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the state of the burn after reading checkbox
          *
-         * @name   topNav.getExpiration
+         * @name   TopNav.getExpiration
          * @function
          * @return {bool}
          */
@@ -2161,7 +2255,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the state of the discussion checkbox
          *
-         * @name   topNav.getOpenDiscussion
+         * @name   TopNav.getOpenDiscussion
          * @function
          * @return {bool}
          */
@@ -2173,7 +2267,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * returns the entered password
          *
-         * @name   topNav.getPassword
+         * @name   TopNav.getPassword
          * @function
          * @return {string}
          */
@@ -2187,7 +2281,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
          *
          * preloads jQuery elements
          *
-         * @name   topNav.init
+         * @name   TopNav.init
          * @function
          */
         me.init = function()
@@ -2216,9 +2310,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             // bind events
             $burnAfterReading.change(changeBurnAfterReading);
             $openDiscussionOption.change(changeOpenDiscussion);
-            $newButton.click(controller.newPaste);
-            $sendButton.click(controller.submitPaste);
-            $cloneButton.click(controller.clonePaste);
+            $newButton.click(Controller.newPaste);
+            $sendButton.click(PasteEncrypter.submitPaste);
+            $cloneButton.click(Controller.clonePaste);
             $rawTextButton.click(rawText);
             $fileRemoveButton.click(me.removeAttachment);
 
@@ -2231,7 +2325,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             changeOpenDiscussion();
 
             // get default value from template or fall back to set value
-            pasteExpiration = modal.getExpirationDefault() || pasteExpiration;
+            pasteExpiration = Modal.getExpirationDefault() || pasteExpiration;
         };
 
         return me;
@@ -2240,21 +2334,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     /**
      * Responsible for AJAX requests, transparently handles encryption…
      *
-     * @name state
      * @class
      */
-    var uploader = (function () {
+    var Uploader = (function () {
         var me = {};
 
         var successFunc = null,
-            failureFunc = null;
-
-        var url = helper.scriptLocation(),
-            data = {},
+            failureFunc = null,
+            url,
+            data,
             randomKey,
             password;
 
-        // public variable ('constant') to prevent magic numbers
+        /**
+         * public variable ('constant') for errors to prevent magic numbers
+         *
+         * @readonly
+         * @enum   {Object}
+         */
         me.error = {
             okay: 0,
             custom: 1,
@@ -2292,7 +2389,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * called after a upload failure
          *
-         * @name   uploader.submitPasteUpload
+         * @name   Uploader.submitPasteUpload
          * @function
          * @param {int} status - internal code
          * @param {int} data - original error code
@@ -2307,12 +2404,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * actually uploads the data
          *
-         * @name   uploader.submitPasteUpload
+         * @name   Uploader.run
          * @function
          */
-        me.trigger = function()
+        me.run = function()
         {
-            console.log(data);
             $.ajax({
                 type: 'POST',
                 url: url,
@@ -2338,7 +2434,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set success function
          *
-         * @name   uploader.setSuccess
+         * @name   Uploader.setSuccess
+         * @function
+         * @param {function} func
+         */
+        me.setUrl = function(newUrl)
+        {
+            url = newUrl;
+        };
+
+        /**
+         * set success function
+         *
+         * @name   Uploader.setSuccess
          * @function
          * @param {function} func
          */
@@ -2350,7 +2458,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set failure function
          *
-         * @name   uploader.setSuccess
+         * @name   Uploader.setSuccess
          * @function
          * @param {function} func
          */
@@ -2362,7 +2470,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * prepares a new upload
          *
-         * @name   uploader.prepare
+         * @name   Uploader.prepare
          * @function
          * @param {string} newPassword
          * @return {object}
@@ -2372,33 +2480,35 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             // set password
             password = newPassword;
 
-            // entropy should already be checked
-            // @TODO maybe move it here?
+            // entropy should already be checked!
 
             // generate a new random key
-            randomKey = cryptTool.getSymmetricKey();
+            randomKey = CryptTool.getSymmetricKey();
 
             // reset data
+            successFunc = null;
+            failureFunc = null;
+            url = Helper.baseUri()
             data = {};
         };
 
         /**
          * encrypts and sets the data
          *
-         * @name   uploader.setData
+         * @name   Uploader.setData
          * @function
          * @param {string} index
          * @param {mixed} element
          */
         me.setData = function(index, element)
         {
-            data[index] = cryptTool.cipher(randomKey, password, element);
+            data[index] = CryptTool.cipher(randomKey, password, element);
         };
 
         /**
          * set the additional metadata to send unencrypted
          *
-         * @name   uploader.setUnencryptedData
+         * @name   Uploader.setUnencryptedData
          * @function
          * @param {string} index
          * @param {mixed} element
@@ -2411,7 +2521,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         /**
          * set the additional metadata to send unencrypted passed at once
          *
-         * @name   uploader.setUnencryptedData
+         * @name   Uploader.setUnencryptedData
          * @function
          * @param {object} newData
          */
@@ -2421,9 +2531,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * init uploader
+         * init Uploader
          *
-         * @name   uploader.init
+         * @name   Uploader.init
          * @function
          */
         me.init = function()
@@ -2435,52 +2545,124 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     })();
 
     /**
-     * PrivateBin logic
+     * (controller) Responsible for encrypting paste and sending it to server.
      *
-     * @param  {object} window
-     * @param  {object} document
-     * @name controller
+     * @name state
      * @class
      */
-    var controller = (function (window, document) {
+    var PasteEncrypter = (function () {
         var me = {};
 
+        var requirementsChecked = false;
+
+        /**
+         * checks whether there is a suitable amount of entrophy
+         *
+         * @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 false;
+            }
+
+            if (!CryptTool.isEntropyReady()) {
+                // display a message and wait
+                Alert.showStatus(I18n._('Please move your mouse for more entropy...'));
+
+                CryptTool.addEntropySeedListener(retryCallback);
+                return false;
+            }
+
+            requirementsChecked = true;
+
+            return true;
+        }
+
         /**
          * called after successful upload
          *
+         * @private
          * @function
          * @param {int} status
          * @param {int} data
          */
         function showCreatedPaste(status, data) {
-            topNav.hideLoading();
-            console.log(data);
+            TopNav.hideLoading();
 
-            var url = helper.scriptLocation() + '?' + data.id + '#' + data.encryptionKey,
-                deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
+            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
+                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
 
-            alert.hideMessages();
+            Alert.hideMessages();
 
             // show notification
-            alert.createPasteNotification(url, deleteUrl)
+            PasteStatus.createPasteNotification(url, deleteUrl)
 
             // show new URL in browser bar
             history.pushState({type: 'newpaste'}, document.title, url);
 
-            topNav.showViewButtons();
-            editor.hide();
+            TopNav.showViewButtons();
+            TopNav.hideRawButton();
+            Editor.hide();
 
             // parse and show text
             // (preparation already done in me.submitPaste())
-            pasteViewer.trigger();
+            PasteViewer.run();
+        }
+
+        /**
+         * adds attachments to the Uploader
+         *
+         * @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(I18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
+                    // cancels process as it does not execute callback
+                    return;
+                }
+
+                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
+                    callback();
+                };
+
+                // actually read first file
+                reader.readAsDataURL(file);
+            } else if (AttachmentViewer.hasAttachment()) {
+                // fall back to cloned part
+                var attachment = AttachmentViewer.getAttachment();
+
+                Uploader.setData('attachment', attachment[0]);
+                Uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??!
+                callback();
+            } else {
+                // if there are no attachments, this is of course still successful
+                callback();
+            }
         }
 
         /**
          * send a reply in a discussion
          *
-         * @name   controller.sendComment
+         * @name   PasteEncrypter.sendComment
          * @function
          * @param  {Event} event
+         * @TODO WIP
          */
         me.sendComment = function(event)
         {
@@ -2493,36 +2675,36 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                 return;
             }
 
-            me.showStatus(i18n._('Sending comment...'), true);
+            me.showStatus(I18n._('Sending comment...'), true);
             var parentid = event.data.parentid,
-                key = helper.pageKey(),
-                cipherdata = cryptTool.cipher(key, $passwordInput.val(), replyMessage.val()),
+                key = Modal.getPasteKey(),
+                cipherdata = CryptTool.cipher(key, $passwordInput.val(), replyMessage.val()),
                 ciphernickname = '',
                 nick = $('#nickname').val();
             if (nick.length > 0)
             {
-                ciphernickname = cryptTool.cipher(key, $passwordInput.val(), nick);
+                ciphernickname = CryptTool.cipher(key, $passwordInput.val(), nick);
             }
             var dataToSend = {
                 data:     cipherdata,
                 parentid: parentid,
-                pasteid:  helper.pasteId(),
+                pasteid:  Modal.getPasteId(),
                 nickname: ciphernickname
             };
 
             $.ajax({
                 type: 'POST',
-                url: helper.scriptLocation(),
+                url: Helper.baseUri(),
                 data: dataToSend,
                 dataType: 'json',
                 headers: ajaxHeaders,
                 success: function(data) {
                     if (data.status === 0)
                     {
-                        controller.showStatus(i18n._('Comment posted.'));
+                        status.showStatus(I18n._('Comment posted.'));
                         $.ajax({
                             type: 'GET',
-                            url: helper.scriptLocation() + '?' + helper.pasteId(),
+                            url: Helper.baseUri() + '?' + Modal.getPasteId(),
                             dataType: 'json',
                             headers: ajaxHeaders,
                             success: function(data) {
@@ -2532,157 +2714,396 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
                                 }
                                 else if (data.status === 1)
                                 {
-                                    alert.showError(i18n._('Could not refresh display: %s', data.message));
+                                    Alert.showError(I18n._('Could not refresh display: %s', data.message));
                                 }
                                 else
                                 {
-                                    alert.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
+                                    Alert.showError(I18n._('Could not refresh display: %s', I18n._('unknown status')));
                                 }
                             }
                         })
                         .fail(function() {
-                            controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
+                            Alert.showError(I18n._('Could not refresh display: %s', I18n._('server error or not responding')));
                         });
                     }
                     else if (data.status === 1)
                     {
-                        controller.showError(i18n._('Could not post comment: %s', data.message));
+                        Alert.showError(I18n._('Could not post comment: %s', data.message));
                     }
                     else
                     {
-                        controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
+                        Alert.showError(I18n._('Could not post comment: %s', I18n._('unknown status')));
                     }
                 }
             })
             .fail(function() {
-                controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
+                Alert.showError(I18n._('Could not post comment: %s', I18n._('server error or not responding')));
             });
         };
 
         /**
          * sends a new paste to server
          *
-         * @name   controller.submitPaste
+         * @name   PasteEncrypter.submitPaste
          * @function
          */
         me.submitPaste = function()
         {
             // UI loading state
-            topNav.hideCreateButtons();
-            topNav.showLoading(i18n._('Sending paste...'), 0);
-            topNav.collapseBar();
+            TopNav.hideCreateButtons();
+            TopNav.showLoading(I18n._('Sending paste...'), 0);
+            TopNav.collapseBar();
 
             // get data
-            var plainText = editor.getText();
+            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…
-                topNav.hideLoading();
-                topNav.showCreateButtons();
+                TopNav.hideLoading();
+                TopNav.showCreateButtons();
                 return;
             }
 
-            topNav.showLoading(i18n._('Sending paste...'), 10);
+            TopNav.showLoading(I18n._('Sending paste...'), 10);
 
             // check entropy
-            if (!cryptTool.isEntropyReady()) {
-                // display a message and wait
-                alert.showStatus(i18n._('Please move your mouse for more entropy...'));
-
-                cryptTool.addEntropySeedListener(function() {
-                    me.submitPaste(event);
-                });
+            if (!checkRequirements(function () {
+                me.submitPaste();
+            })) {
+                return; // to prevent multiple executions
             }
 
-            // prepare uploader
-            uploader.prepare(topNav.getPassword());
-
-            // encrypt cipher data
-            uploader.setData('data', plainText);
-
-            // encrypt attachments
-            var files = topNav.getFileList();
-            if (files !== null) {
-                var reader = new FileReader();
-
-                // closure to capture the file information
-                reader.onload = (function(file) {
-                    return function(event) {
-                        uploader.setData('attachment', event.target.result);
-                        uploader.setData('attachmentname', file.name);
-                    };
-                })(files[0]);
-
-                // actually read first file
-                reader.readAsDataURL(files[0]);
-            } else if (alert.hasAttachment()) {
-                var attachment = alert.getAttachment();
-
-                uploader.setData('attachment', attachment[0]);
-                uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??!
-            }
+            // prepare Uploader
+            Uploader.prepare(TopNav.getPassword());
 
             // set success/fail functions
-            uploader.setSuccess(showCreatedPaste);
-            uploader.setFailure(function (status, data) {
+            Uploader.setSuccess(showCreatedPaste);
+            Uploader.setFailure(function (status, data) {
                 // revert loading status…
-                topNav.hideLoading();
-                topNav.showCreateButtons();
+                TopNav.hideLoading();
+                TopNav.showCreateButtons();
 
                 // show error message
                 switch (status) {
-                    case uploader.error['custom']:
-                        alert.showError(i18n._('Could not create paste: %s', data.message));
+                    case Uploader.error['custom']:
+                        Alert.showError(I18n._('Could not create paste: %s', data.message));
                         break;
-                    case uploader.error['unknown']:
-                        alert.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
+                    case Uploader.error['unknown']:
+                        Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown status')));
                         break;
-                    case uploader.error['serverError']:
-                        alert.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
+                    case Uploader.error['serverError']:
+                        Alert.showError(I18n._('Could not create paste: %s', I18n._('server error or not responding')));
                         break;
                     default:
-                        alert.showError(i18n._('Could not create paste: %s', i18n._('unknown error')));
+                        Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown error')));
                         break;
                 }
             });
 
             // fill it with unencrypted submitted options
-            var format = pasteViewer.getFormat();
-            uploader.setUnencryptedBulkData({
-                expire:           topNav.getExpiration(),
+            Uploader.setUnencryptedBulkData({
+                expire:           TopNav.getExpiration(),
                 formatter:        format,
-                burnafterreading: topNav.getBurnAfterReading() ? 1 : 0,
-                opendiscussion:   topNav.getOpenDiscussion() ? 1 : 0
+                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
+                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
             });
 
             // prepare PasteViewer for later preview
-            pasteViewer.setText(plainText);
-            pasteViewer.setFormat(format);
+            PasteViewer.setText(plainText);
+            PasteViewer.setFormat(format);
 
-            // send data
-            uploader.trigger();
+            // 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;
+    })();
+
+    /**
+     * (controller) Responsible for decrypting cipherdata and passing data to view.
+     *
+     * @name state
+     * @class
+     */
+    var PasteDecrypter = (function () {
+        var me = {};
+
+        /**
+         * decrypt the actual paste text
+         *
+         * @private
+         * @function
+         * @param {object} paste - paste data in object form
+         * @param {string} key
+         * @param {string} password
+         * @return {bool} - whether action was successful
+         */
+        function decryptPaste(paste, key, password)
+        {
+            // try decryption without password
+            var plaintext = CryptTool.decipher(key, password, paste.data);
+
+            // if it fails, request password
+            if (plaintext.length === 0 && password.length === 0) {
+                // get password
+                password = Prompt.getPassword();
+
+                // if password is there, re-try
+                if (password.length !== 0) {
+                    // recursive
+                    // note: an infinite loop is prevented as the previous if
+                    // clause checks whether a password is already set and ignores
+                    // error with password being passed
+                    return decryptPaste(paste, key, password);
+                }
+
+                // trigger password request
+                Prompt.requestPassword();
+                // the callback (via setPasswordCallback()) should have been set
+                // by parent function
+                return false;
+            }
+
+            // if all tries failed, we can only throw an error
+            if (plaintext.length === 0) {
+                throw 'failed to decipher message';
+            }
+
+            // 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
+         *
+         * @private
+         * @function
+         * @param {object} paste - paste data in object form
+         * @param {string} key
+         * @param {string} password
+         * @return {bool} - whether action was successful
+         */
+        function decryptAttachment(paste, key, password)
+        {
+            // decrypt attachment
+            var attachment = CryptTool.decipher(key, password, paste.attachment);
+            if (attachment.length === 0) {
+                throw 'failed to decipher attachment';
+            }
+
+            // decrypt attachment name
+            var attachmentName;
+            if (paste.attachmentname) {
+                attachmentName = attachmentName = CryptTool.decipher(key, password, paste.attachmentname);
+                if (attachmentName.length === 0) {
+                    // @TODO considering the buggy cloning (?, see other todo comment) this might affect previous pastes
+                    throw 'failed to decipher attachment name';
+                }
+            }
+
+            AttachmentViewer.setAttachment(attachment, attachmentName);
+            AttachmentViewer.showAttachment();
+        }
+
+        /**
+         * 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)
+        {
+            TopNav.showLoading('Decrypting paste…');
+
+            if (typeof paste === 'undefined') {
+                paste = $.parseJSON(Modal.getCipherData());
+            }
+
+            var key = Modal.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 {
+                Prompt.setPasswordCallback(function () {
+                    me.run(paste);
+                });
+
+                // try to decrypt paste and if it fails (because the password is
+                // missing) return to let JS continue and wait for user
+                if (!decryptPaste(paste, key, password)) {
+                    return;
+                }
+
+                // decrypt attachments
+                if (paste.attachment) {
+                    decryptAttachment(paste, key, password);
+                }
+            } catch(err) {
+                TopNav.hideLoading();
+
+                // log and show error
+                console.error(err);
+                Alert.showError(I18n._('Could not decrypt data (Wrong key?)')); // @TODO error is not translated
+
+                // still go on to potentially show potentially partially decrypted data
+            }
+
+            // shows the remaining time (until) deletion
+            PasteStatus.showRemainingTime(paste.meta);
+
+            // if the discussion is opened on this paste, display it
+            // @TODO BELOW
+            if (paste.meta.opendiscussion) {
+                $comments.html('');
+
+                var $divComment;
+
+                // iterate over comments
+                for (var i = 0; i < paste.comments.length; ++i)
+                {
+                    var $place = $comments,
+                        comment = paste.comments[i],
+                        commentText = CryptTool.decipher(key, password, comment.data),
+                        $parentComment = $('#comment_' + comment.parentid);
+
+                    $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>');
+                    var $divCommentData = $divComment.find('div.commentdata');
+
+                    // if parent comment exists
+                    if ($parentComment.length)
+                    {
+                        // shift comment to the right
+                        $place = $parentComment;
+                    }
+                    $divComment.find('button').click({commentid: comment.id}, me.openReply);
+                    Helper.setElementText($divCommentData, commentText);
+                    Helper.urls2links($divCommentData);
+
+                    // try to get optional nickname
+                    var nick = CryptTool.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);
+                }
+
+                // add 'add new comment' area
+                $divComment = $(
+                    '<div class="comment"><button class="btn btn-default btn-sm">' +
+                    I18n._('Add comment') + '</button></div>'
+                );
+                $divComment.find('button').click({commentid: Modal.getPasteId()}, me.openReply);
+                $comments.append($divComment);
+                $discussion.removeClass('hidden');
+            }
+
+            TopNav.hideLoading();
+            TopNav.showViewButtons();
+        };
+
+        /**
+         * initialize
+         *
+         * @name   PasteDecrypter.init
+         * @function
+         */
+        me.init = function()
+        {
+            // nothing yet
+        };
+
+        return me;
+    })();
+
+    /**
+     * (controller) main PrivateBin logic
+     *
+     * @param  {object} window
+     * @param  {object} document
+     * @class
+     */
+    var Controller = (function (window, document) {
+        var me = {};
+
         /**
          * creates a new paste
          *
-         * @name   controller.newPaste
+         * @name   Controller.newPaste
          * @function
          */
         me.newPaste = function()
         {
-            topNav.hideViewButtons();
-            topNav.showCreateButtons();
-            editor.resetInput();
-            editor.show();
-            editor.focusInput();
+            TopNav.hideViewButtons();
+            TopNav.showCreateButtons();
+            PasteViewer.hide();
+            Editor.resetInput();
+            Editor.show();
+            Editor.focusInput();
+            Alert.hideMessages();
         };
 
         /**
          * clone the current paste
          *
-         * @name   controller.clonePaste
+         * @name   Controller.clonePaste
          * @function
          * @param  {Event} event
          */
@@ -2691,64 +3112,79 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
             me.stateNewPaste();
 
             // erase the id and the key in url
-            history.replaceState(null, document.title, helper.scriptLocation());
+            history.replaceState(null, document.title, Helper.baseUri());
 
-            alert.hideMessages();
+            Alert.hideMessages();
             if ($attachmentLink.attr('href'))
             {
                 $clonedFile.removeClass('hidden');
                 $fileWrap.addClass('hidden');
             }
             $message.val(
-                pasteViewer.getFormat() === 'markdown' ?
-                    $prettyPrint.val() : $clearText.val()
+                PasteViewer.getFormat() === 'markdown' ?
+                    $prettyPrint.val() : $plainText.val()
             );
-            $('.navbar-toggle').click();
+            TopNav.collapseBar();
+        };
 
-            event.preventDefault();
+        /**
+         * 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
+         * @name   Controller.init
          * @function
          */
         me.init = function()
         {
             // first load translations
-            i18n.loadTranslations();
+            I18n.loadTranslations();
 
             // initialize other modules/"classes"
-            alert.init();
-            uploader.init();
-            modal.init();
-            cryptTool.init();
-            uiMan.init();
-            topNav.init();
-            editor.init();
-            pasteViewer.init();
-            prompt.init();
+            Alert.init();
+            Uploader.init();
+            Modal.init();
+            CryptTool.init();
+            UiHelper.init();
+            TopNav.init();
+            Editor.init();
+            PasteStatus.init();
+            PasteViewer.init();
+            AttachmentViewer.init();
+            DiscussionViewer.init();
+            PasteEncrypter.init();
+            PasteDecrypter.init();
+            Prompt.init();
 
             // display an existing paste
-            if (modal.hasCipherData()) {
+            if (Modal.hasCipherData()) {
                 // missing decryption key in URL?
-                if (window.location.hash.length === 0)
-                {
-                    alert.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?)'));
+                if (window.location.hash.length === 0) {
+                    Alert.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?)'));
                     return;
                 }
 
                 // show proper elements on screen
-                // topNav.hideCreateButtons(); // they should not be visible in the first place
-                topNav.showViewButtons();
-                me.displayMessages();
-                return;
-            }
-
-            // check requirements for upload
-            if (typeof FileReader === 'undefined') {
-                alert.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
+                PasteDecrypter.run();
                 return;
             }
 
@@ -2759,21 +3195,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         return me;
     })(window, document);
 
-    jQuery(document).ready(function() {
-        /**
-         * main application start, called when DOM is fully loaded and
-         * runs controller initalization
-         */
-        $(controller.init);
-    });
-
     return {
-        helper: helper,
-        i18n: i18n,
-        cryptTool: cryptTool,
-        topNav: topNav,
-        alert: alert,
-        uploader: uploader,
-        controller: controller
+        Helper: Helper,
+        I18n: I18n,
+        CryptTool: CryptTool,
+        TopNav: TopNav,
+        Alert: Alert,
+        Uploader: Uploader,
+        Controller: Controller
     };
 }(jQuery, sjcl, Base64, RawDeflate);
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index b9e3f60..75275b5 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -132,7 +132,7 @@ if ($isPage):
 							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Send'), PHP_EOL;
 else:
 ?>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
+						<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;
 ?>
@@ -399,7 +399,7 @@ endif;
 				<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
 				<?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
 			</div>
-			<div id="errormessage" role="alert" class="<?php echo empty($ERROR) ? 'hidden' : '' ?> alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span><span><?php echo htmlspecialchars($ERROR); ?></span></div>
+			<div id="errormessage" role="alert" class="<?php echo empty($ERROR) ? 'hidden' : '' ?> alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span><span> <?php echo htmlspecialchars($ERROR); ?></span></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)); ?></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)); ?></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; ?>
@@ -432,11 +432,11 @@ endif;
 		<section class="container">
 			<article class="row">
 				<div id="placeholder" class="col-md-12 hidden"><?php echo I18n::_('Nothing to see… Try to enter some text.'); ?></div>
-				<div id="image" class="col-md-12 text-center hidden"></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="cleartext" class="col-md-12 hidden"></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>