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

Revert "JS: tried namespaces"

This reverts commit e84cfc58a16d56b2deb9450c5ca8033a9a4b9b37.
rugk 9 лет назад
Родитель
Сommit
52f1fb143e
1 измененных файлов с 1745 добавлено и 1741 удалено
  1. 1745 1741
      js/privatebin.js

+ 1745 - 1741
js/privatebin.js

@@ -25,1944 +25,1948 @@
 // Immediately start random number generator collector.
 sjcl.random.startCollectors();
 
-// startup
-jQuery(document).ready(function() {
-    /**
-     * main application start, called when DOM is fully loaded and
-     * runs controller initalization after translations are loaded
-     */
-    PrivateBin.i18n.loadTranslations();
-});
-
-/**
- * @name PrivateBin
- * @namespace
- */
-var PrivateBin = window.PrivateBin || {};
-
-/**
- * static helper methods
- *
- * @param  {object} window
- * @param  {object} document
- * @name helper
- * @class
- */
-PrivateBin.helper = (function (window, document, jQuery, sjcl, Base64, RawDeflate) {
-    var me = {};
-
-    /**
-     * character to HTML entity lookup table
-     *
-     * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
-     * @private
-     * @enum   {Object}
-     * @readonly
-     */
-    var entityMap = {
-        '&': '&',
-        '<': '&lt;',
-        '>': '&gt;',
-        '"': '&quot;',
-        "'": '&#39;',
-        '/': '&#x2F;',
-        '`': '&#x60;',
-        '=': '&#x3D;'
-    };
+// jQuery(document).ready(function() {
+//     // startup
+// }
 
+jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
     /**
-     * cache for script location
+     * static helper methods
      *
-     * @private
-     * @enum   {string|null}
+     * @param  {object} window
+     * @param  {object} document
+     * @name helper
+     * @class
      */
-    var scriptLocation = null;
+    var helper = (function (window, document) {
+        var me = {};
+
+        /**
+         * character to HTML entity lookup table
+         *
+         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
+         * @private
+         * @enum   {Object}
+         * @readonly
+         */
+        var entityMap = {
+            '&': '&amp;',
+            '<': '&lt;',
+            '>': '&gt;',
+            '"': '&quot;',
+            "'": '&#39;',
+            '/': '&#x2F;',
+            '`': '&#x60;',
+            '=': '&#x3D;'
+        };
 
-    /**
-     * converts a duration (in seconds) into human friendly approximation
-     *
-     * @name helper.secondsToHuman
-     * @function
-     * @param  {number} seconds
-     * @return {Array}
-     */
-    me.secondsToHuman = function(seconds)
-    {
-        var v;
-        if (seconds < 60)
-        {
-            v = Math.floor(seconds);
-            return [v, 'second'];
-        }
-        if (seconds < 60 * 60)
-        {
-            v = Math.floor(seconds / 60);
-            return [v, 'minute'];
-        }
-        if (seconds < 60 * 60 * 24)
+        /**
+         * cache for script location
+         *
+         * @private
+         * @enum   {string|null}
+         */
+        var scriptLocation = null;
+
+        /**
+         * converts a duration (in seconds) into human friendly approximation
+         *
+         * @name helper.secondsToHuman
+         * @function
+         * @param  {number} seconds
+         * @return {Array}
+         */
+        me.secondsToHuman = function(seconds)
         {
-            v = Math.floor(seconds / (60 * 60));
-            return [v, 'hour'];
-        }
-        // If less than 2 months, display in days:
-        if (seconds < 60 * 60 * 24 * 60)
+            var v;
+            if (seconds < 60)
+            {
+                v = Math.floor(seconds);
+                return [v, 'second'];
+            }
+            if (seconds < 60 * 60)
+            {
+                v = Math.floor(seconds / 60);
+                return [v, 'minute'];
+            }
+            if (seconds < 60 * 60 * 24)
+            {
+                v = Math.floor(seconds / (60 * 60));
+                return [v, 'hour'];
+            }
+            // If less than 2 months, display in days:
+            if (seconds < 60 * 60 * 24 * 60)
+            {
+                v = Math.floor(seconds / (60 * 60 * 24));
+                return [v, 'day'];
+            }
+            v = Math.floor(seconds / (60 * 60 * 24 * 30));
+            return [v, 'month'];
+        };
+
+        /**
+         * 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
+         * @function
+         * @param  {HTMLElement} element
+         */
+        me.selectText = function(element)
         {
-            v = Math.floor(seconds / (60 * 60 * 24));
-            return [v, 'day'];
-        }
-        v = Math.floor(seconds / (60 * 60 * 24 * 30));
-        return [v, 'month'];
-    };
+            var range, selection;
 
-    /**
-     * 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
-     * @function
-     * @param  {HTMLElement} element
-     */
-    me.selectText = function(element)
-    {
-        var range, selection;
+            // MS
+            if (document.body.createTextRange)
+            {
+                range = document.body.createTextRange();
+                range.moveToElementText(element);
+                range.select();
+            }
+            // all others
+            else if (window.getSelection)
+            {
+                selection = window.getSelection();
+                range = document.createRange();
+                range.selectNodeContents(element);
+                selection.removeAllRanges();
+                selection.addRange(range);
+            }
+        };
 
-        // MS
-        if (document.body.createTextRange)
-        {
-            range = document.body.createTextRange();
-            range.moveToElementText(element);
-            range.select();
-        }
-        // all others
-        else if (window.getSelection)
+        /**
+         * set text of a jQuery element (required for IE),
+         *
+         * @name   helper.setElementText
+         * @function
+         * @param  {jQuery} $element - a jQuery element
+         * @param  {string} text - the text to enter
+         * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look
+         */
+        me.setElementText = function($element, text)
         {
-            selection = window.getSelection();
-            range = document.createRange();
-            range.selectNodeContents(element);
-            selection.removeAllRanges();
-            selection.addRange(range);
-        }
-    };
+            // 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>');
+                $element.html('<pre>' + html + '</pre>');
+            }
+            // for other (sane) browsers:
+            else
+            {
+                $element.text(text);
+            }
+        };
 
-    /**
-     * set text of a jQuery element (required for IE),
-     *
-     * @name   helper.setElementText
-     * @function
-     * @param  {jQuery} $element - a jQuery element
-     * @param  {string} text - the text to enter
-     * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look
-     */
-    me.setElementText = function($element, text)
-    {
-        // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
-        if ($('#oldienotice').is(':visible')) {
-            var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
-            $element.html('<pre>' + html + '</pre>');
-        }
-        // for other (sane) browsers:
-        else
+        /**
+         * replace last child of element with message
+         *
+         * @name   helper.setMessage
+         * @function
+         * @param  {jQuery} $element - a jQuery wrapped DOM element
+         * @param  {string} message - the message to append
+         */
+        me.setMessage = function($element, message)
         {
-            $element.text(text);
-        }
-    };
+            var content = $element.contents();
+            if (content.length > 0)
+            {
+                content[content.length - 1].nodeValue = ' ' + message;
+            }
+            else
+            {
+                me.setElementText($element, message);
+            }
+        };
 
-    /**
-     * replace last child of element with message
-     *
-     * @name   helper.setMessage
-     * @function
-     * @param  {jQuery} $element - a jQuery wrapped DOM element
-     * @param  {string} message - the message to append
-     */
-    me.setMessage = function($element, message)
-    {
-        var content = $element.contents();
-        if (content.length > 0)
-        {
-            content[content.length - 1].nodeValue = ' ' + message;
-        }
-        else
+        /**
+         * convert URLs to clickable links.
+         * URLs to handle:
+         * <pre>
+         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
+         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         * </pre>
+         *
+         * @name   helper.urls2links
+         * @function
+         * @param  {Object} element - a jQuery DOM element
+         */
+        me.urls2links = function(element)
         {
-            me.setElementText($element, message);
-        }
-    };
-
-    /**
-     * convert URLs to clickable links.
-     * URLs to handle:
-     * <pre>
-     *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
-     *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-     *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-     * </pre>
-     *
-     * @name   helper.urls2links
-     * @function
-     * @param  {Object} element - a jQuery DOM element
-     */
-    me.urls2links = function(element)
-    {
-        var markup = '<a href="$1" rel="nofollow">$1</a>';
-        element.html(
-            element.html().replace(
-                /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
-                markup
-            )
-        );
-        element.html(
-            element.html().replace(
-                /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
-                markup
-            )
-        );
-    };
+            var markup = '<a href="$1" rel="nofollow">$1</a>';
+            element.html(
+                element.html().replace(
+                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
+                    markup
+                )
+            );
+            element.html(
+                element.html().replace(
+                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
+                    markup
+                )
+            );
+        };
 
-    /**
-     * 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
-     * @function
-     * @param  {string} format
-     * @param  {...*} args - one or multiple parameters injected into format string
-     * @return {string}
-     */
-    me.sprintf = function()
-    {
-        var args = arguments;
-        if (typeof arguments[0] === 'object')
+        /**
+         * 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
+         * @function
+         * @param  {string} format
+         * @param  {...*} args - one or multiple parameters injected into format string
+         * @return {string}
+         */
+        me.sprintf = function()
         {
-            args = arguments[0];
-        }
-        var format = args[0],
-            i = 1;
-        return format.replace(/%((%)|s|d)/g, function (m) {
-            // m is the matched format, e.g. %s, %d
-            var val;
-            if (m[2]) {
-                val = m[2];
-            } else {
-                val = args[i];
-                // A switch statement so that the formatter can be extended.
-                switch (m)
+            var args = arguments;
+            if (typeof arguments[0] === 'object')
+            {
+                args = arguments[0];
+            }
+            var format = args[0],
+                i = 1;
+            return format.replace(/%((%)|s|d)/g, function (m) {
+                // m is the matched format, e.g. %s, %d
+                var val;
+                if (m[2]) {
+                    val = m[2];
+                } else {
+                    val = args[i];
+                    // A switch statement so that the formatter can be extended.
+                    switch (m)
+                    {
+                        case '%d':
+                            val = parseFloat(val);
+                            if (isNaN(val)) {
+                                val = 0;
+                            }
+                            break;
+                        default:
+                            // Default is %s
+                    }
+                    ++i;
+                }
+                return val;
+            });
+        };
+
+        /**
+         * get value of cookie, if it was set, empty string otherwise
+         *
+         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
+         * @name   helper.getCookie
+         * @function
+         * @param  {string} cname
+         * @return {string}
+         */
+        me.getCookie = function(cname) {
+            var name = cname + '=',
+                ca = document.cookie.split(';');
+            for (var i = 0; i < ca.length; ++i) {
+                var c = ca[i];
+                while (c.charAt(0) === ' ')
                 {
-                    case '%d':
-                        val = parseFloat(val);
-                        if (isNaN(val)) {
-                            val = 0;
-                        }
-                        break;
-                    default:
-                        // Default is %s
+                    c = c.substring(1);
+                }
+                if (c.indexOf(name) === 0)
+                {
+                    return c.substring(name.length, c.length);
                 }
-                ++i;
             }
-            return val;
-        });
-    };
+            return '';
+        };
 
-    /**
-     * get value of cookie, if it was set, empty string otherwise
-     *
-     * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
-     * @name   helper.getCookie
-     * @function
-     * @param  {string} cname
-     * @return {string}
-     */
-    me.getCookie = function(cname) {
-        var name = cname + '=',
-            ca = document.cookie.split(';');
-        for (var i = 0; i < ca.length; ++i) {
-            var c = ca[i];
-            while (c.charAt(0) === ' ')
-            {
-                c = c.substring(1);
+        /**
+         * get the current script location (without search or hash part of the URL),
+         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
+         *
+         * @name   helper.scriptLocation
+         * @function
+         * @return {string} current script location
+         */
+        me.scriptLocation = function()
+        {
+            // check for cached version
+            if (scriptLocation !== null) {
+                return scriptLocation;
             }
-            if (c.indexOf(name) === 0)
+
+            scriptLocation = window.location.href.substring(
+                    0,
+                    window.location.href.length - window.location.search.length - window.location.hash.length
+                );
+
+            var hashIndex = scriptLocation.indexOf('?');
+
+            if (hashIndex !== -1)
             {
-                return c.substring(name.length, c.length);
+                scriptLocation = scriptLocation.substring(0, hashIndex);
             }
-        }
-        return '';
-    };
 
-    /**
-     * get the current script location (without search or hash part of the URL),
-     * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
-     *
-     * @name   helper.scriptLocation
-     * @function
-     * @return {string} current script location
-     */
-    me.scriptLocation = function()
-    {
-        // check for cached version
-        if (scriptLocation !== null) {
             return scriptLocation;
-        }
-
-        scriptLocation = window.location.href.substring(
-                0,
-                window.location.href.length - window.location.search.length - window.location.hash.length
-            );
-
-        var hashIndex = scriptLocation.indexOf('?');
+        };
 
-        if (hashIndex !== -1)
+        /**
+         * 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()
         {
-            scriptLocation = scriptLocation.substring(0, hashIndex);
-        }
-
-        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 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)
+        /**
+         * return the deciphering key stored in anchor part of the URL
+         *
+         * @name   helper.pageKey
+         * @function
+         * @return {string} key
+         */
+        me.pageKey = function()
         {
-            key = key.substring(0, i);
-        }
-
-        return key;
-    };
-
-    /**
-     * convert all applicable characters to HTML entities
-     *
-     * @see    {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
-     * @name   helper.htmlEntities
-     * @function
-     * @param  {string} str
-     * @return {string} escaped HTML
-     */
-    me.htmlEntities = function(str) {
-        return String(str).replace(
-            /[&<>"'`=\/]/g, function(s) {
-                return entityMap[s];
-            });
-    };
-
-    return me;
-})(window, document, jQuery, sjcl, Base64, RawDeflate);
-
-/**
- * internationalization methods
- *
- * @param  {object} window
- * @param  {object} document
- * @name i18n
- * @class
- */
-PrivateBin.i18n = (function (window, document, jQuery, sjcl, Base64, RawDeflate) {
-    var me = {};
+            var key = window.location.hash.substring(1),
+                i = key.indexOf('&');
 
-    /**
-     * supported languages, minus the built in 'en'
-     *
-     * @private
-     * @prop   {string[]}
-     * @readonly
-     */
-    var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh'];
+            // 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);
+            }
 
-    /**
-     * built in language
-     *
-     * @private
-     * @prop   {string}
-     */
-    var language = 'en';
+            return key;
+        };
 
-    /**
-     * translation cache
-     *
-     * @private
-     * @enum   {Object}
-     */
-    var translations = {};
+        /**
+         * convert all applicable characters to HTML entities
+         *
+         * @see    {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
+         * @name   helper.htmlEntities
+         * @function
+         * @param  {string} str
+         * @return {string} escaped HTML
+         */
+        me.htmlEntities = function(str) {
+            return String(str).replace(
+                /[&<>"'`=\/]/g, function(s) {
+                    return entityMap[s];
+                });
+        };
 
-    /**
-     * translate a string, alias for i18n.translate()
-     *
-     * @name   i18n._
-     * @function
-     * @param  {string} messageId
-     * @param  {...*} args - one or multiple parameters injected into placeholders
-     * @return {string}
-     */
-    me._ = function()
-    {
-        return me.translate(arguments);
-    };
+        return me;
+    })(window, document);
 
     /**
-     * translate a string
+     * internationalization methods
      *
-     * @name   i18n.translate
-     * @function
-     * @param  {string} messageId
-     * @param  {...*} args - one or multiple parameters injected into placeholders
-     * @return {string}
+     * @param  {object} window
+     * @param  {object} document
+     * @name i18n
+     * @class
      */
-    me.translate = function()
-    {
-        var args = arguments, messageId;
-        if (typeof arguments[0] === 'object')
-        {
-            args = arguments[0];
-        }
-        var usesPlurals = $.isArray(args[0]);
-        if (usesPlurals)
-        {
-            // use the first plural form as messageId, otherwise the singular
-            messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
-        }
-        else
+    var i18n = (function (window, document) {
+        var me = {};
+
+        /**
+         * supported languages, minus the built in 'en'
+         *
+         * @private
+         * @prop   {string[]}
+         * @readonly
+         */
+        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh'];
+
+        /**
+         * built in language
+         *
+         * @private
+         * @prop   {string}
+         */
+        var language = 'en';
+
+        /**
+         * translation cache
+         *
+         * @private
+         * @enum   {Object}
+         */
+        var translations = {};
+
+        /**
+         * translate a string, alias for i18n.translate()
+         *
+         * @name   i18n._
+         * @function
+         * @param  {string} messageId
+         * @param  {...*} args - one or multiple parameters injected into placeholders
+         * @return {string}
+         */
+        me._ = function()
         {
-            messageId = args[0];
-        }
-        if (messageId.length === 0)
-        {
-            return messageId;
-        }
-        if (!translations.hasOwnProperty(messageId))
+            return me.translate(arguments);
+        };
+
+        /**
+         * translate a string
+         *
+         * @name   i18n.translate
+         * @function
+         * @param  {string} messageId
+         * @param  {...*} args - one or multiple parameters injected into placeholders
+         * @return {string}
+         */
+        me.translate = function()
         {
-            if (language !== 'en')
+            var args = arguments, messageId;
+            if (typeof arguments[0] === 'object')
             {
-                console.error(
-                    'Missing ' + language + ' translation for: ' + messageId
-                );
+                args = arguments[0];
             }
-            translations[messageId] = args[0];
-        }
-        if (usesPlurals && $.isArray(translations[messageId]))
-        {
-            var n = parseInt(args[1] || 1, 10),
-                key = me.getPluralForm(n),
-                maxKey = translations[messageId].length - 1;
-            if (key > maxKey)
+            var usesPlurals = $.isArray(args[0]);
+            if (usesPlurals)
             {
-                key = maxKey;
+                // use the first plural form as messageId, otherwise the singular
+                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
             }
-            args[0] = translations[messageId][key];
-            args[1] = n;
-        }
-        else
-        {
-            args[0] = translations[messageId];
-        }
-        return helper.sprintf(args);
-    };
-
-    /**
-     * 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
-     * @function
-     * @param  {number} n
-     * @return {number} array key
-     */
-    me.getPluralForm = function(n) {
-        switch (language)
-        {
-            case 'fr':
-            case 'oc':
-            case 'zh':
-                return (n > 1 ? 1 : 0);
-            case 'pl':
-                return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
-            case 'ru':
-                return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
-            case 'sl':
-                return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
-            // de, en, es, it, no
-            default:
-                return (n !== 1 ? 1 : 0);
-        }
-    };
-
-    /**
-     * load translations into cache, then trigger controller initialization
-     *
-     * @name   i18n.loadTranslations
-     * @function
-     */
-    me.loadTranslations = function()
-    {
-        var newLanguage = PrivateBin.helper.getCookie('lang');
-
-        // auto-select language based on browser settings
-        if (newLanguage.length === 0)
-        {
-            newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
-        }
+            else
+            {
+                messageId = args[0];
+            }
+            if (messageId.length === 0)
+            {
+                return messageId;
+            }
+            if (!translations.hasOwnProperty(messageId))
+            {
+                if (language !== 'en')
+                {
+                    console.error(
+                        'Missing ' + language + ' translation for: ' + messageId
+                    );
+                }
+                translations[messageId] = args[0];
+            }
+            if (usesPlurals && $.isArray(translations[messageId]))
+            {
+                var n = parseInt(args[1] || 1, 10),
+                    key = me.getPluralForm(n),
+                    maxKey = translations[messageId].length - 1;
+                if (key > maxKey)
+                {
+                    key = maxKey;
+                }
+                args[0] = translations[messageId][key];
+                args[1] = n;
+            }
+            else
+            {
+                args[0] = translations[messageId];
+            }
+            return helper.sprintf(args);
+        };
 
-        // if language is already used (e.g, default 'en'), skip update
-        if (newLanguage === language)
-        {
-            controller.init();
-            return;
-        }
+        /**
+         * 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
+         * @function
+         * @param  {number} n
+         * @return {number} array key
+         */
+        me.getPluralForm = function(n) {
+            switch (language)
+            {
+                case 'fr':
+                case 'oc':
+                case 'zh':
+                    return (n > 1 ? 1 : 0);
+                case 'pl':
+                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
+                case 'ru':
+                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
+                case 'sl':
+                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
+                // de, en, es, it, no
+                default:
+                    return (n !== 1 ? 1 : 0);
+            }
+        };
 
-        // if language is not supported, show error
-        if (supportedLanguages.indexOf(newLanguage) === -1)
+        /**
+         * load translations into cache, then trigger controller initialization
+         *
+         * @name   i18n.loadTranslations
+         * @function
+         */
+        me.loadTranslations = function()
         {
-            console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
-            controller.init();
-        }
+            var newLanguage = helper.getCookie('lang');
 
-        // load strongs from JSON
-        $.getJSON('i18n/' + newLanguage + '.json', function(data) {
-            language = newLanguage;
-            translations = data;
-        }).fail(function (data, textStatus, errorMsg) {
-            console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
-        });
+            // auto-select language based on browser settings
+            if (newLanguage.length === 0)
+            {
+                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
+            }
 
-        controller.init();
-    };
+            // if language is already used (e.g, default 'en'), skip update
+            if (newLanguage === language)
+            {
+                controller.init();
+                return;
+            }
 
-    return me;
-})(window, document, jQuery, sjcl, Base64, RawDeflate);
+            // 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);
+                controller.init();
+            }
 
-/**
- * filter methods
- *
- * @param  {object} window
- * @param  {object} document
- * @name filter
- * @class
- */
-PrivateBin.filter = (function (window, document, jQuery, sjcl, Base64, RawDeflate) {
-    var me = {};
+            // load strongs from JSON
+            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
+                language = newLanguage;
+                translations = data;
+            }).fail(function (data, textStatus, errorMsg) {
+                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
+            });
 
-    /**
-     * compress a message (deflate compression), returns base64 encoded data
-     *
-     * @name   filter.compress
-     * @function
-     * @param  {string} message
-     * @return {string} base64 data
-     */
-    me.compress = function(message)
-    {
-        return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
-    };
+            controller.init();
+        };
 
-    /**
-     * decompress a message compressed with filter.compress()
-     *
-     * @name   filter.decompress
-     * @function
-     * @param  {string} data - base64 data
-     * @return {string} message
-     */
-    me.decompress = function(data)
-    {
-        return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
-    };
+        return me;
+    })(window, document);
 
     /**
-     * compress, then encrypt message with given key and password
+     * filter methods
      *
-     * @name   filter.cipher
-     * @function
-     * @param  {string} key
-     * @param  {string} password
-     * @param  {string} message
-     * @return {string} data - JSON with encrypted data
+     * @param  {object} window
+     * @param  {object} document
+     * @name filter
+     * @class
      */
-    me.cipher = function(key, password, message)
-    {
-        // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
-        var options = {mode: 'gcm', ks: 256, ts: 128};
-        if ((password || '').trim().length === 0)
+    var filter = (function (window, document) {
+        var me = {};
+
+        /**
+         * compress a message (deflate compression), returns base64 encoded data
+         *
+         * @name   filter.compress
+         * @function
+         * @param  {string} message
+         * @return {string} base64 data
+         */
+        me.compress = function(message)
         {
-            return sjcl.encrypt(key, me.compress(message), options);
-        }
-        return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options);
-    };
-
-    /**
-     * decrypt message with key, then decompress
-     *
-     * @name   filter.decipher
-     * @function
-     * @param  {string} key
-     * @param  {string} password
-     * @param  {string} data - JSON with encrypted data
-     * @return {string} decrypted message
-     */
-    me.decipher = function(key, password, data)
-    {
-        if (data !== undefined)
+            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
+        },
+
+        /**
+         * decompress a message compressed with filter.compress()
+         *
+         * @name   filter.decompress
+         * @function
+         * @param  {string} data - base64 data
+         * @return {string} message
+         */
+        me.decompress = function(data)
         {
-            try
+            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
+        },
+
+        /**
+         * compress, then encrypt message with given key and password
+         *
+         * @name   filter.cipher
+         * @function
+         * @param  {string} key
+         * @param  {string} password
+         * @param  {string} message
+         * @return {string} data - JSON with encrypted data
+         */
+        me.cipher = function(key, password, message)
+        {
+            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
+            var options = {mode: 'gcm', ks: 256, ts: 128};
+            if ((password || '').trim().length === 0)
             {
-                return me.decompress(sjcl.decrypt(key, data));
+                return sjcl.encrypt(key, me.compress(message), options);
             }
-            catch(err)
+            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options);
+        },
+
+        /**
+         * decrypt message with key, then decompress
+         *
+         * @name   filter.decipher
+         * @function
+         * @param  {string} key
+         * @param  {string} password
+         * @param  {string} data - JSON with encrypted data
+         * @return {string} decrypted message
+         */
+        me.decipher = function(key, password, data)
+        {
+            if (data !== undefined)
             {
                 try
                 {
-                    return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
+                    return me.decompress(sjcl.decrypt(key, data));
                 }
-                catch(e)
+                catch(err)
                 {
-                    // ignore error, because ????? @TODO
+                    try
+                    {
+                        return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
+                    }
+                    catch(e)
+                    {
+                        // ignore error, because ????? @TODO
+                    }
                 }
             }
+            return '';
         }
-        return '';
-    };
 
-    return me;
-})(window, document, jQuery, sjcl, Base64, RawDeflate);
-
-/**
- * PrivateBin logic
- *
- * @param  {object} window
- * @param  {object} document
- * @name controller
- * @class
- */
-PrivateBin.controller = (function (window, document, jQuery, sjcl, Base64, RawDeflate) {
-    var me = {};
-
-    /**
-     * headers to send in AJAX requests
-     *
-     * @private
-     * @enum   {Object}
-     */
-    var headers = {'X-Requested-With': 'JSONHttpRequest'};
-
-    /**
-     * URL shortners create address
-     *
-     * @private
-     * @prop   {string}
-     */
-    var shortenerUrl = '';
-
-    /**
-     * URL of newly created paste
-     *
-     * @private
-     * @prop   {string}
-     */
-    var createdPasteUrl = '';
-
-    // jQuery pre-loaded objects
-    var $attach,
-        $attachment,
-        $attachmentLink,
-        $burnAfterReading,
-        $burnAfterReadingOption,
-        $cipherData,
-        $clearText,
-        $cloneButton,
-        $clonedFile,
-        $comments,
-        $discussion,
-        $errorMessage,
-        $expiration,
-        $fileRemoveButton,
-        $fileWrap,
-        $formatter,
-        $image,
-        $loadingIndicator,
-        $message,
-        $messageEdit,
-        $messagePreview,
-        $newButton,
-        $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear
-        $openDiscussion,
-        $password,
-        $passwordInput,
-        $passwordModal,
-        $passwordForm,
-        $passwordDecrypt,
-        $pasteResult,
-        $pasteUrl,
-        $prettyMessage,
-        $prettyPrint,
-        $preview,
-        $rawTextButton,
-        $remainingTime,
-        $replyStatus,
-        $sendButton,
-        $status;
+        return me;
+    })(window, document);
 
     /**
-     * ask the user for the password and set it
+     * PrivateBin logic
      *
-     * @name   controller.requestPassword
-     * @function
+     * @param  {object} window
+     * @param  {object} document
+     * @name controller
+     * @class
      */
-    me.requestPassword = function()
-    {
-        if ($passwordModal.length === 0) {
-            var password = prompt(i18n._('Please enter the password for this paste:'), '');
-            if (password === null)
-            {
-                throw 'password prompt canceled';
-            }
-            if (password.length === 0)
-            {
-                // recursive…
-                me.requestPassword();
+    var controller = (function (window, document) {
+        var me = {};
+
+        /**
+         * headers to send in AJAX requests
+         *
+         * @private
+         * @enum   {Object}
+         */
+        var headers = {'X-Requested-With': 'JSONHttpRequest'};
+
+        /**
+         * URL shortners create address
+         *
+         * @private
+         * @prop   {string}
+         */
+        var shortenerUrl = '';
+
+        /**
+         * URL of newly created paste
+         *
+         * @private
+         * @prop   {string}
+         */
+        var createdPasteUrl = '';
+
+        // jQuery pre-loaded objects
+        var $attach,
+            $attachment,
+            $attachmentLink,
+            $burnAfterReading,
+            $burnAfterReadingOption,
+            $cipherData,
+            $clearText,
+            $cloneButton,
+            $clonedFile,
+            $comments,
+            $discussion,
+            $errorMessage,
+            $expiration,
+            $fileRemoveButton,
+            $fileWrap,
+            $formatter,
+            $image,
+            $loadingIndicator,
+            $message,
+            $messageEdit,
+            $messagePreview,
+            $newButton,
+            $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear
+            $openDiscussion,
+            $password,
+            $passwordInput,
+            $passwordModal,
+            $passwordForm,
+            $passwordDecrypt,
+            $pasteResult,
+            $pasteUrl,
+            $prettyMessage,
+            $prettyPrint,
+            $preview,
+            $rawTextButton,
+            $remainingTime,
+            $replyStatus,
+            $sendButton,
+            $status;
+
+        /**
+         * ask the user for the password and set it
+         *
+         * @name   controller.requestPassword
+         * @function
+         */
+        me.requestPassword = function()
+        {
+            if ($passwordModal.length === 0) {
+                var password = prompt(i18n._('Please enter the password for this paste:'), '');
+                if (password === null)
+                {
+                    throw 'password prompt canceled';
+                }
+                if (password.length === 0)
+                {
+                    // recursive…
+                    me.requestPassword();
+                } else {
+                    $passwordInput.val(password);
+                    me.displayMessages();
+                }
             } else {
-                $passwordInput.val(password);
-                me.displayMessages();
+                $passwordModal.modal();
             }
-        } else {
-            $passwordModal.modal();
-        }
-    };
+        };
 
-    /**
-     * use given format on paste, defaults to plain text
-     *
-     * @name   controller.formatPaste
-     * @function
-     * @param  {string} format
-     * @param  {string} text
-     */
-    me.formatPaste = function(format, text)
-    {
-        helper.setElementText($clearText, text);
-        helper.setElementText($prettyPrint, text);
-
-        switch (format || 'plaintext') {
-            case 'markdown':
-                // silently fail if showdown is not available
-                // @TODO: maybe better show an error message? At least a warning?
-                if (typeof showdown === 'object')
-                {
-                    var converter = new showdown.Converter({
-                        strikethrough: true,
-                        tables: true,
-                        tablesHeaderId: true
-                    });
-                    $clearText.html(
-                        converter.makeHtml(text)
-                    );
-                    // add table classes from bootstrap css
-                    $clearText.find('table').addClass('table-condensed table-bordered');
+        /**
+         * use given format on paste, defaults to plain text
+         *
+         * @name   controller.formatPaste
+         * @function
+         * @param  {string} format
+         * @param  {string} text
+         */
+        me.formatPaste = function(format, text)
+        {
+            helper.setElementText($clearText, text);
+            helper.setElementText($prettyPrint, text);
+
+            switch (format || 'plaintext') {
+                case 'markdown':
+                    // silently fail if showdown is not available
+                    // @TODO: maybe better show an error message? At least a warning?
+                    if (typeof showdown === 'object')
+                    {
+                        var converter = new showdown.Converter({
+                            strikethrough: true,
+                            tables: true,
+                            tablesHeaderId: true
+                        });
+                        $clearText.html(
+                            converter.makeHtml(text)
+                        );
+                        // add table classes from bootstrap css
+                        $clearText.find('table').addClass('table-condensed table-bordered');
 
-                    $clearText.removeClass('hidden');
-                } else {
-                    console.error('showdown is not loaded, could not parse Markdown');
-                }
-                $prettyMessage.addClass('hidden');
-                break;
-            case 'syntaxhighlighting':
-                // silently fail if prettyprint is not available
-                // @TODO: maybe better show an error message? At least a warning?
-                if (typeof prettyPrintOne === 'function')
-                {
-                    if (typeof prettyPrint === 'function')
+                        $clearText.removeClass('hidden');
+                    } else {
+                        console.error('showdown is not loaded, could not parse Markdown');
+                    }
+                    $prettyMessage.addClass('hidden');
+                    break;
+                case 'syntaxhighlighting':
+                    // silently fail if prettyprint is not available
+                    // @TODO: maybe better show an error message? At least a warning?
+                    if (typeof prettyPrintOne === 'function')
                     {
-                        prettyPrint();
+                        if (typeof prettyPrint === 'function')
+                        {
+                            prettyPrint();
+                        }
+                        $prettyPrint.html(
+                            prettyPrintOne(
+                                helper.htmlEntities(text), null, true
+                            )
+                        );
+                    } else {
+                        console.error('pretty print is not loaded, could not link ');
                     }
-                    $prettyPrint.html(
-                        prettyPrintOne(
-                            helper.htmlEntities(text), null, true
-                        )
-                    );
-                } else {
-                    console.error('pretty print is not loaded, could not link ');
-                }
-                // fall through, as the rest is the same
-            default: // = 'plaintext'
-                // convert URLs to clickable links
-                helper.urls2links($clearText);
-                helper.urls2links($prettyPrint);
-                $clearText.addClass('hidden');
+                    // fall through, as the rest is the same
+                default: // = 'plaintext'
+                    // convert URLs to clickable links
+                    helper.urls2links($clearText);
+                    helper.urls2links($prettyPrint);
+                    $clearText.addClass('hidden');
 
 
-                $prettyPrint.css('white-space', 'pre-wrap');
-                $prettyPrint.css('word-break', 'normal');
-                $prettyPrint.removeClass('prettyprint');
+                    $prettyPrint.css('white-space', 'pre-wrap');
+                    $prettyPrint.css('word-break', 'normal');
+                    $prettyPrint.removeClass('prettyprint');
 
-                $prettyMessage.removeClass('hidden');
-        }
-    };
+                    $prettyMessage.removeClass('hidden');
+            }
+        };
 
-    /**
-     * show decrypted text in the display area, including discussion (if open)
-     *
-     * @name   controller.displayMessages
-     * @function
-     * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
-     */
-    me.displayMessages = function(paste)
-    {
-        paste = paste || $.parseJSON($cipherData.text());
-        var key = helper.pageKey(),
-            password = $passwordInput.val();
-        if (!$prettyPrint.hasClass('prettyprinted')) {
-            // Try to decrypt the paste.
-            try
-            {
-                if (paste.attachment)
+        /**
+         * show decrypted text in the display area, including discussion (if open)
+         *
+         * @name   controller.displayMessages
+         * @function
+         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
+         */
+        me.displayMessages = function(paste)
+        {
+            paste = paste || $.parseJSON($cipherData.text());
+            var key = helper.pageKey(),
+                password = $passwordInput.val();
+            if (!$prettyPrint.hasClass('prettyprinted')) {
+                // Try to decrypt the paste.
+                try
                 {
-                    var attachment = filter.decipher(key, password, paste.attachment);
-                    if (attachment.length === 0)
+                    if (paste.attachment)
                     {
-                        if (password.length === 0)
+                        var attachment = filter.decipher(key, password, paste.attachment);
+                        if (attachment.length === 0)
+                        {
+                            if (password.length === 0)
+                            {
+                                me.requestPassword();
+                                return;
+                            }
+                            attachment = filter.decipher(key, password, paste.attachment);
+                        }
+                        if (attachment.length === 0)
+                        {
+                            throw 'failed to decipher attachment';
+                        }
+
+                        if (paste.attachmentname)
                         {
-                            me.requestPassword();
-                            return;
+                            var attachmentname = filter.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');
                         }
-                        attachment = filter.decipher(key, password, paste.attachment);
                     }
-                    if (attachment.length === 0)
+                    var cleartext = filter.decipher(key, password, paste.data);
+                    if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
                     {
-                        throw 'failed to decipher attachment';
+                        me.requestPassword();
+                        return;
                     }
-
-                    if (paste.attachmentname)
+                    if (cleartext.length === 0 && !paste.attachment)
                     {
-                        var attachmentname = filter.decipher(key, password, paste.attachmentname);
-                        if (attachmentname.length > 0)
-                        {
-                            $attachmentLink.attr('download', attachmentname);
-                        }
+                        throw 'failed to decipher message';
                     }
-                    $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)
+                    $passwordInput.val(password);
+                    if (cleartext.length > 0)
                     {
-                        $image.html(
-                            $(document.createElement('img'))
-                                .attr('src', attachment)
-                                .attr('class', 'img-thumbnail')
-                        );
-                        $image.removeClass('hidden');
+                        $('#pasteFormatter').val(paste.meta.formatter);
+                        me.formatPaste(paste.meta.formatter, cleartext);
                     }
                 }
-                var cleartext = filter.decipher(key, password, paste.data);
-                if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
+                catch(err)
                 {
-                    me.requestPassword();
+                    me.stateOnlyNewPaste();
+                    me.showError(i18n._('Could not decrypt data (Wrong key?)'));
                     return;
                 }
-                if (cleartext.length === 0 && !paste.attachment)
-                {
-                    throw 'failed to decipher message';
-                }
+            }
+
+            // display paste expiration / for your eyes only
+            if (paste.meta.expire_date)
+            {
+                var expiration = helper.secondsToHuman(paste.meta.remaining_time),
+                    expirationLabel = [
+                        'This document will expire in %d ' + expiration[1] + '.',
+                        'This document will expire in %d ' + expiration[1] + 's.'
+                    ];
+                helper.setMessage($remainingTime, 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.'));
+                });
+                helper.setMessage($remainingTime, 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;
 
-                $passwordInput.val(password);
-                if (cleartext.length > 0)
+                // iterate over comments
+                for (var i = 0; i < paste.comments.length; ++i)
                 {
-                    $('#pasteFormatter').val(paste.meta.formatter);
-                    me.formatPaste(paste.meta.formatter, cleartext);
+                    var $place = $comments,
+                        comment = paste.comments[i],
+                        commentText = filter.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 = filter.decipher(key, password, comment.meta.nickname);
+                    if (nick.length > 0)
+                    {
+                        $divComment.find('span.nickname').text(nick);
+                    }
+                    else
+                    {
+                        divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
+                    }
+                    $divComment.find('span.commentdate')
+                              .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
+                              .attr('title', 'CommentID: ' + comment.id);
+
+                    // if an avatar is available, display it
+                    if (comment.meta.vizhash)
+                    {
+                        $divComment.find('span.nickname')
+                                  .before(
+                                    '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
+                                    i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
+                                  );
+                    }
+
+                    $place.append($divComment);
                 }
+
+                // 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');
             }
-            catch(err)
+        };
+
+        /**
+         * open the comment entry when clicking the "Reply" button of a comment
+         *
+         * @name   controller.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 = $(
+                    '<div class="reply"><input type="text" id="nickname" ' +
+                    'class="form-control" title="' + hint + '" placeholder="' +
+                    hint + '" /><textarea id="replymessage" class="replymessage ' +
+                    'form-control" cols="80" rows="7"></textarea><br />' +
+                    '<div id="replystatus"></div><button id="replybutton" ' +
+                    'class="btn btn-default btn-sm">' + i18n._('Post comment') +
+                    '</button></div>'
+                );
+            reply.find('button').click(
+                {parentid: commentid},
+                me.sendComment
+            );
+            source.after(reply);
+            $replyStatus = $('#replystatus');
+            $('#replymessage').focus();
+        };
+
+        /**
+         * send a reply in a discussion
+         *
+         * @name   controller.sendComment
+         * @function
+         * @param  {Event} event
+         */
+        me.sendComment = function(event)
+        {
+            event.preventDefault();
+            $errorMessage.addClass('hidden');
+            // do not send if no data
+            var replyMessage = $('#replymessage');
+            if (replyMessage.val().length === 0)
             {
-                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.'
-                ];
-            helper.setMessage($remainingTime, 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
+            me.showStatus(i18n._('Sending comment...'), true);
+            var parentid = event.data.parentid,
+                key = helper.pageKey(),
+                cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()),
+                ciphernickname = '',
+                nick = $('#nickname').val();
+            if (nick.length > 0)
+            {
+                ciphernickname = filter.cipher(key, $passwordInput.val(), nick);
+            }
+            var data_to_send = {
+                data:     cipherdata,
+                parentid: parentid,
+                pasteid:  helper.pasteId(),
+                nickname: ciphernickname
+            };
+
             $.ajax({
                 type: 'POST',
-                url: helper.scriptLocation() + '?' + helper.pasteId(),
-                data: {deletetoken: 'burnafterreading'},
+                url: helper.scriptLocation(),
+                data: data_to_send,
                 dataType: 'json',
-                headers: headers
+                headers: headers,
+                success: function(data)
+                {
+                    if (data.status === 0)
+                    {
+                        controller.showStatus(i18n._('Comment posted.'));
+                        $.ajax({
+                            type: 'GET',
+                            url: helper.scriptLocation() + '?' + helper.pasteId(),
+                            dataType: 'json',
+                            headers: headers,
+                            success: function(data)
+                            {
+                                if (data.status === 0)
+                                {
+                                    controller.displayMessages(data);
+                                }
+                                else if (data.status === 1)
+                                {
+                                    controller.showError(i18n._('Could not refresh display: %s', data.message));
+                                }
+                                else
+                                {
+                                    controller.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')));
+                        });
+                    }
+                    else if (data.status === 1)
+                    {
+                        controller.showError(i18n._('Could not post comment: %s', data.message));
+                    }
+                    else
+                    {
+                        controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
+                    }
+                }
             })
             .fail(function() {
-                controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
+                controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
             });
-            helper.setMessage($remainingTime, 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)
+        /**
+         * send a new paste to server
+         *
+         * @name   controller.sendData
+         * @function
+         * @param  {Event} event
+         */
+        me.sendData = function(event)
         {
-            $comments.html('');
+            event.preventDefault();
+            var file = document.getElementById('file'),
+                files = (file && file.files) ? file.files : null; // FileList object
 
-            var $divComment;
+            // do not send if no data.
+            if ($message.val().length === 0 && !(files && files[0]))
+            {
+                return;
+            }
 
-            // iterate over comments
-            for (var i = 0; i < paste.comments.length; ++i)
+            // if sjcl has not collected enough entropy yet, display a message
+            if (!sjcl.random.isReady())
             {
-                var $place = $comments,
-                    comment = paste.comments[i],
-                    commentText = filter.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);
+                me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
+                sjcl.random.addEventListener('seeded', function() {
+                    me.sendData(event);
+                });
+                return;
+            }
+
+            $('.navbar-toggle').click();
+            $password.addClass('hidden');
+            me.showStatus(i18n._('Sending paste...'), true);
 
-                // try to get optional nickname
-                var nick = filter.decipher(key, password, comment.meta.nickname);
-                if (nick.length > 0)
+            me.stateSubmittingPaste();
+
+            var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0),
+                password = $passwordInput.val();
+            if(files && files[0])
+            {
+                if(typeof FileReader === undefined)
                 {
-                    $divComment.find('span.nickname').text(nick);
+                    // revert loading status…
+                    me.stateNewPaste();
+                    me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
+                    return;
                 }
-                else
+                var reader = new FileReader();
+                // closure to capture the file information
+                reader.onload = (function(theFile)
                 {
-                    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);
+                    return function(e) {
+                        controller.sendDataContinue(
+                            randomkey,
+                            filter.cipher(randomkey, password, e.target.result),
+                            filter.cipher(randomkey, password, theFile.name)
+                        );
+                    };
+                })(files[0]);
+                reader.readAsDataURL(files[0]);
+            }
+            else if($attachmentLink.attr('href'))
+            {
+                me.sendDataContinue(
+                    randomkey,
+                    filter.cipher(randomkey, password, $attachmentLink.attr('href')),
+                    $attachmentLink.attr('download')
+                );
+            }
+            else
+            {
+                me.sendDataContinue(randomkey, '', '');
+            }
+        };
 
-                // if an avatar is available, display it
-                if (comment.meta.vizhash)
+        /**
+         * send a new paste to server, step 2
+         *
+         * @name   controller.sendDataContinue
+         * @function
+         * @param  {string} randomkey
+         * @param  {string} cipherdata_attachment
+         * @param  {string} cipherdata_attachment_name
+         */
+        me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
+        {
+            var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()),
+                data_to_send = {
+                    data:             cipherdata,
+                    expire:           $('#pasteExpiration').val(),
+                    formatter:        $('#pasteFormatter').val(),
+                    burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0,
+                    opendiscussion:   $openDiscussion.is(':checked') ? 1 : 0
+                };
+            if (cipherdata_attachment.length > 0)
+            {
+                data_to_send.attachment = cipherdata_attachment;
+                if (cipherdata_attachment_name.length > 0)
                 {
-                    $divComment.find('span.nickname')
-                              .before(
-                                '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
-                                i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
-                              );
+                    data_to_send.attachmentname = cipherdata_attachment_name;
                 }
-
-                $place.append($divComment);
             }
+            $.ajax({
+                type: 'POST',
+                url: helper.scriptLocation(),
+                data: data_to_send,
+                dataType: 'json',
+                headers: headers,
+                success: function(data)
+                {
+                    if (data.status === 0) {
+                        me.stateExistingPaste();
+                        var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey,
+                            deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
+                        me.showStatus('');
+                        $errorMessage.addClass('hidden');
+                        // show new URL in browser bar
+                        history.pushState({type: 'newpaste'}, document.title, url);
+
+                        $('#pastelink').html(
+                            i18n._(
+                                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
+                                url, url
+                            ) + me.shortenUrl(url)
+                        );
+                        // save newly created element
+                        $pasteUrl = $('#pasteurl');
+                        // and add click event
+                        $pasteUrl.click(me.pasteLinkClick);
+
+                        var shortenButton = $('#shortenbutton');
+                        if (shortenButton) {
+                            shortenButton.click(me.sendToShortener);
+                        }
+                        $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
+                        $pasteResult.removeClass('hidden');
+                        // we pre-select the link so that the user only has to [Ctrl]+[c] the link
+                        helper.selectText($pasteUrl[0]);
+                        me.showStatus('');
+                        me.formatPaste(data_to_send.formatter, $message.val());
+                    }
+                    else if (data.status === 1)
+                    {
+                        // revert loading status…
+                        controller.stateNewPaste();
+                        controller.showError(i18n._('Could not create paste: %s', data.message));
+                    }
+                    else
+                    {
+                        // revert loading status…
+                        controller.stateNewPaste();
+                        controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
+                    }
+                }
+            })
+            .fail(function()
+            {
+                // revert loading status…
+                me.stateNewPaste();
+                controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
+            });
+        };
 
-            // 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');
-        }
-    };
+        /**
+         * check if a URL shortener was defined and create HTML containing a link to it
+         *
+         * @name   controller.shortenUrl
+         * @function
+         * @param  {string} url
+         * @return {string} html
+         */
+        me.shortenUrl = function(url)
+        {
+            var shortenerHtml = $('#shortenbutton');
+            if (shortenerHtml) {
+                shortenerUrl = shortenerHtml.data('shortener');
+                createdPasteUrl = url;
+                return ' ' + $('<div />').append(shortenerHtml.clone()).html();
+            }
+            return '';
+        };
 
-    /**
-     * open the comment entry when clicking the "Reply" button of a comment
-     *
-     * @name   controller.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 = $(
-                '<div class="reply"><input type="text" id="nickname" ' +
-                'class="form-control" title="' + hint + '" placeholder="' +
-                hint + '" /><textarea id="replymessage" class="replymessage ' +
-                'form-control" cols="80" rows="7"></textarea><br />' +
-                '<div id="replystatus"></div><button id="replybutton" ' +
-                'class="btn btn-default btn-sm">' + i18n._('Post comment') +
-                '</button></div>'
-            );
-        reply.find('button').click(
-            {parentid: commentid},
-            me.sendComment
-        );
-        source.after(reply);
-        $replyStatus = $('#replystatus');
-        $('#replymessage').focus();
-    };
+        /**
+         * put the screen in "New paste" mode
+         *
+         * @name   controller.stateNewPaste
+         * @function
+         */
+        me.stateNewPaste = function()
+        {
+            $message.text('');
+            $attachment.addClass('hidden');
+            $cloneButton.addClass('hidden');
+            $rawTextButton.addClass('hidden');
+            $remainingTime.addClass('hidden');
+            $pasteResult.addClass('hidden');
+            $clearText.addClass('hidden');
+            $discussion.addClass('hidden');
+            $prettyMessage.addClass('hidden');
+            $loadingIndicator.addClass('hidden');
+            $sendButton.removeClass('hidden');
+            $expiration.removeClass('hidden');
+            $formatter.removeClass('hidden');
+            $burnAfterReadingOption.removeClass('hidden');
+            $openDisc.removeClass('hidden');
+            $newButton.removeClass('hidden');
+            $password.removeClass('hidden');
+            $attach.removeClass('hidden');
+            $message.removeClass('hidden');
+            $preview.removeClass('hidden');
+            $message.focus();
+        };
 
-    /**
-     * send a reply in a discussion
-     *
-     * @name   controller.sendComment
-     * @function
-     * @param  {Event} event
-     */
-    me.sendComment = function(event)
-    {
-        event.preventDefault();
-        $errorMessage.addClass('hidden');
-        // do not send if no data
-        var replyMessage = $('#replymessage');
-        if (replyMessage.val().length === 0)
+        /**
+         * put the screen in mode after submitting a paste
+         *
+         * @name   controller.stateSubmittingPaste
+         * @function
+         */
+        me.stateSubmittingPaste = function()
         {
-            return;
-        }
+            $message.text('');
+            $attachment.addClass('hidden');
+            $cloneButton.addClass('hidden');
+            $rawTextButton.addClass('hidden');
+            $remainingTime.addClass('hidden');
+            $pasteResult.addClass('hidden');
+            $clearText.addClass('hidden');
+            $discussion.addClass('hidden');
+            $prettyMessage.addClass('hidden');
+            $sendButton.addClass('hidden');
+            $expiration.addClass('hidden');
+            $formatter.addClass('hidden');
+            $burnAfterReadingOption.addClass('hidden');
+            $openDisc.addClass('hidden');
+            $newButton.addClass('hidden');
+            $password.addClass('hidden');
+            $attach.addClass('hidden');
+            $message.addClass('hidden');
+            $preview.addClass('hidden');
 
-        me.showStatus(i18n._('Sending comment...'), true);
-        var parentid = event.data.parentid,
-            key = helper.pageKey(),
-            cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()),
-            ciphernickname = '',
-            nick = $('#nickname').val();
-        if (nick.length > 0)
+            $loadingIndicator.removeClass('hidden');
+        };
+
+        /**
+         * put the screen in a state where the only option is to submit a
+         * new paste
+         *
+         * @name   controller.stateOnlyNewPaste
+         * @function
+         */
+        me.stateOnlyNewPaste = function()
         {
-            ciphernickname = filter.cipher(key, $passwordInput.val(), nick);
-        }
-        var data_to_send = {
-            data:     cipherdata,
-            parentid: parentid,
-            pasteid:  helper.pasteId(),
-            nickname: ciphernickname
+            $message.text('');
+            $attachment.addClass('hidden');
+            $cloneButton.addClass('hidden');
+            $rawTextButton.addClass('hidden');
+            $remainingTime.addClass('hidden');
+            $pasteResult.addClass('hidden');
+            $clearText.addClass('hidden');
+            $discussion.addClass('hidden');
+            $prettyMessage.addClass('hidden');
+            $sendButton.addClass('hidden');
+            $expiration.addClass('hidden');
+            $formatter.addClass('hidden');
+            $burnAfterReadingOption.addClass('hidden');
+            $openDisc.addClass('hidden');
+            $password.addClass('hidden');
+            $attach.addClass('hidden');
+            $message.addClass('hidden');
+            $preview.addClass('hidden');
+            $loadingIndicator.addClass('hidden');
+
+            $newButton.removeClass('hidden');
         };
 
-        $.ajax({
-            type: 'POST',
-            url: helper.scriptLocation(),
-            data: data_to_send,
-            dataType: 'json',
-            headers: headers,
-            success: function(data)
+        /**
+         * put the screen in "Existing paste" mode
+         *
+         * @name   controller.stateExistingPaste
+         * @function
+         * @param  {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false
+         */
+        me.stateExistingPaste = function(preview)
+        {
+            preview = preview || false;
+
+            if (!preview)
             {
-                if (data.status === 0)
-                {
-                    controller.showStatus(i18n._('Comment posted.'));
-                    $.ajax({
-                        type: 'GET',
-                        url: helper.scriptLocation() + '?' + helper.pasteId(),
-                        dataType: 'json',
-                        headers: headers,
-                        success: function(data)
-                        {
-                            if (data.status === 0)
-                            {
-                                controller.displayMessages(data);
-                            }
-                            else if (data.status === 1)
-                            {
-                                controller.showError(i18n._('Could not refresh display: %s', data.message));
-                            }
-                            else
-                            {
-                                controller.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')));
-                    });
-                }
-                else if (data.status === 1)
+                // no "clone" for IE<10.
+                if ($('#oldienotice').is(":visible"))
                 {
-                    controller.showError(i18n._('Could not post comment: %s', data.message));
+                    $cloneButton.addClass('hidden');
                 }
                 else
                 {
-                    controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
+                    $cloneButton.removeClass('hidden');
                 }
+
+                $rawTextButton.removeClass('hidden');
+                $sendButton.addClass('hidden');
+                $attach.addClass('hidden');
+                $expiration.addClass('hidden');
+                $formatter.addClass('hidden');
+                $burnAfterReadingOption.addClass('hidden');
+                $openDisc.addClass('hidden');
+                $newButton.removeClass('hidden');
+                $preview.addClass('hidden');
             }
-        })
-        .fail(function() {
-            controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
-        });
-    };
 
-    /**
-     * send a new paste to server
-     *
-     * @name   controller.sendData
-     * @function
-     * @param  {Event} event
-     */
-    me.sendData = function(event)
-    {
-        event.preventDefault();
-        var file = document.getElementById('file'),
-            files = (file && file.files) ? file.files : null; // FileList object
-
-        // do not send if no data.
-        if ($message.val().length === 0 && !(files && files[0]))
-        {
-            return;
-        }
+            $pasteResult.addClass('hidden');
+            $message.addClass('hidden');
+            $clearText.addClass('hidden');
+            $prettyMessage.addClass('hidden');
+            $loadingIndicator.addClass('hidden');
+        };
 
-        // if sjcl has not collected enough entropy yet, display a message
-        if (!sjcl.random.isReady())
+        /**
+         * when "burn after reading" is checked, disable discussion
+         *
+         * @name   controller.changeBurnAfterReading
+         * @function
+         */
+        me.changeBurnAfterReading = function()
         {
-            me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
-            sjcl.random.addEventListener('seeded', function() {
-                me.sendData(event);
-            });
-            return;
-        }
-
-        $('.navbar-toggle').click();
-        $password.addClass('hidden');
-        me.showStatus(i18n._('Sending paste...'), true);
-
-        me.stateSubmittingPaste();
+            if ($burnAfterReading.is(':checked') )
+            {
+                $openDisc.addClass('buttondisabled');
+                $openDiscussion.attr({checked: false, disabled: true});
+            }
+            else
+            {
+                $openDisc.removeClass('buttondisabled');
+                $openDiscussion.removeAttr('disabled');
+            }
+        };
 
-        var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0),
-            password = $passwordInput.val();
-        if(files && files[0])
+        /**
+         * when discussion is checked, disable "burn after reading"
+         *
+         * @name   controller.changeOpenDisc
+         * @function
+         */
+        me.changeOpenDisc = function()
         {
-            if(typeof FileReader === undefined)
+            if ($openDiscussion.is(':checked') )
             {
-                // revert loading status…
-                me.stateNewPaste();
-                me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
-                return;
+                $burnAfterReadingOption.addClass('buttondisabled');
+                $burnAfterReading.attr({checked: false, disabled: true});
             }
-            var reader = new FileReader();
-            // closure to capture the file information
-            reader.onload = (function(theFile)
+            else
             {
-                return function(e) {
-                    controller.sendDataContinue(
-                        randomkey,
-                        filter.cipher(randomkey, password, e.target.result),
-                        filter.cipher(randomkey, password, theFile.name)
-                    );
-                };
-            })(files[0]);
-            reader.readAsDataURL(files[0]);
-        }
-        else if($attachmentLink.attr('href'))
-        {
-            me.sendDataContinue(
-                randomkey,
-                filter.cipher(randomkey, password, $attachmentLink.attr('href')),
-                $attachmentLink.attr('download')
-            );
-        }
-        else
-        {
-            me.sendDataContinue(randomkey, '', '');
-        }
-    };
-
-    /**
-     * send a new paste to server, step 2
-     *
-     * @name   controller.sendDataContinue
-     * @function
-     * @param  {string} randomkey
-     * @param  {string} cipherdata_attachment
-     * @param  {string} cipherdata_attachment_name
-     */
-    me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
-    {
-        var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()),
-            data_to_send = {
-                data:             cipherdata,
-                expire:           $('#pasteExpiration').val(),
-                formatter:        $('#pasteFormatter').val(),
-                burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0,
-                opendiscussion:   $openDiscussion.is(':checked') ? 1 : 0
-            };
-        if (cipherdata_attachment.length > 0)
-        {
-            data_to_send.attachment = cipherdata_attachment;
-            if (cipherdata_attachment_name.length > 0)
-            {
-                data_to_send.attachmentname = cipherdata_attachment_name;
-            }
-        }
-        $.ajax({
-            type: 'POST',
-            url: helper.scriptLocation(),
-            data: data_to_send,
-            dataType: 'json',
-            headers: headers,
-            success: function(data)
-            {
-                if (data.status === 0) {
-                    me.stateExistingPaste();
-                    var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey,
-                        deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
-                    me.showStatus('');
-                    $errorMessage.addClass('hidden');
-                    // show new URL in browser bar
-                    history.pushState({type: 'newpaste'}, document.title, url);
-
-                    $('#pastelink').html(
-                        i18n._(
-                            'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
-                            url, url
-                        ) + me.shortenUrl(url)
-                    );
-                    // save newly created element
-                    $pasteUrl = $('#pasteurl');
-                    // and add click event
-                    $pasteUrl.click(me.pasteLinkClick);
-
-                    var shortenButton = $('#shortenbutton');
-                    if (shortenButton) {
-                        shortenButton.click(me.sendToShortener);
-                    }
-                    $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
-                    $pasteResult.removeClass('hidden');
-                    // we pre-select the link so that the user only has to [Ctrl]+[c] the link
-                    helper.selectText($pasteUrl[0]);
-                    me.showStatus('');
-                    me.formatPaste(data_to_send.formatter, $message.val());
-                }
-                else if (data.status === 1)
-                {
-                    // revert loading status…
-                    controller.stateNewPaste();
-                    controller.showError(i18n._('Could not create paste: %s', data.message));
-                }
-                else
-                {
-                    // revert loading status…
-                    controller.stateNewPaste();
-                    controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
-                }
+                $burnAfterReadingOption.removeClass('buttondisabled');
+                $burnAfterReading.removeAttr('disabled');
             }
-        })
-        .fail(function()
+        };
+
+        /**
+         * forward to URL shortener
+         *
+         * @name   controller.sendToShortener
+         * @function
+         * @param  {Event} event
+         */
+        me.sendToShortener = function(event)
         {
-            // revert loading status…
-            me.stateNewPaste();
-            controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
-        });
-    };
+            window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl);
+            event.preventDefault();
+        };
 
-    /**
-     * check if a URL shortener was defined and create HTML containing a link to it
-     *
-     * @name   controller.shortenUrl
-     * @function
-     * @param  {string} url
-     * @return {string} html
-     */
-    me.shortenUrl = function(url)
-    {
-        var shortenerHtml = $('#shortenbutton');
-        if (shortenerHtml) {
-            shortenerUrl = shortenerHtml.data('shortener');
-            createdPasteUrl = url;
-            return ' ' + $('<div />').append(shortenerHtml.clone()).html();
-        }
-        return '';
-    };
+        /**
+         * reload the page
+         *
+         * This takes the user to the PrivateBin home page.
+         *
+         * @name   controller.reloadPage
+         * @function
+         * @param  {Event} event
+         */
+        me.reloadPage = function(event)
+        {
+            window.location.href = helper.scriptLocation();
+            event.preventDefault();
+        };
 
-    /**
-     * put the screen in "New paste" mode
-     *
-     * @name   controller.stateNewPaste
-     * @function
-     */
-    me.stateNewPaste = function()
-    {
-        $message.text('');
-        $attachment.addClass('hidden');
-        $cloneButton.addClass('hidden');
-        $rawTextButton.addClass('hidden');
-        $remainingTime.addClass('hidden');
-        $pasteResult.addClass('hidden');
-        $clearText.addClass('hidden');
-        $discussion.addClass('hidden');
-        $prettyMessage.addClass('hidden');
-        $loadingIndicator.addClass('hidden');
-        $sendButton.removeClass('hidden');
-        $expiration.removeClass('hidden');
-        $formatter.removeClass('hidden');
-        $burnAfterReadingOption.removeClass('hidden');
-        $openDisc.removeClass('hidden');
-        $newButton.removeClass('hidden');
-        $password.removeClass('hidden');
-        $attach.removeClass('hidden');
-        $message.removeClass('hidden');
-        $preview.removeClass('hidden');
-        $message.focus();
-    };
+        /**
+         * return raw text
+         *
+         * @name   controller.rawText
+         * @function
+         * @param  {Event} event
+         */
+        me.rawText = function(event)
+        {
+            var paste = $('#pasteFormatter').val() === 'markdown' ?
+                $prettyPrint.text() : $clearText.text();
+            history.pushState(
+                null, document.title, helper.scriptLocation() + '?' +
+                helper.pasteId() + '#' + helper.pageKey()
+            );
+            // we use text/html instead of text/plain to avoid a bug when
+            // reloading the raw text view (it reverts to type text/html)
+            var newDoc = document.open('text/html', 'replace');
+            newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
+            newDoc.close();
 
-    /**
-     * put the screen in mode after submitting a paste
-     *
-     * @name   controller.stateSubmittingPaste
-     * @function
-     */
-    me.stateSubmittingPaste = function()
-    {
-        $message.text('');
-        $attachment.addClass('hidden');
-        $cloneButton.addClass('hidden');
-        $rawTextButton.addClass('hidden');
-        $remainingTime.addClass('hidden');
-        $pasteResult.addClass('hidden');
-        $clearText.addClass('hidden');
-        $discussion.addClass('hidden');
-        $prettyMessage.addClass('hidden');
-        $sendButton.addClass('hidden');
-        $expiration.addClass('hidden');
-        $formatter.addClass('hidden');
-        $burnAfterReadingOption.addClass('hidden');
-        $openDisc.addClass('hidden');
-        $newButton.addClass('hidden');
-        $password.addClass('hidden');
-        $attach.addClass('hidden');
-        $message.addClass('hidden');
-        $preview.addClass('hidden');
-
-        $loadingIndicator.removeClass('hidden');
-    };
+            event.preventDefault();
+        };
 
-    /**
-     * put the screen in a state where the only option is to submit a
-     * new paste
-     *
-     * @name   controller.stateOnlyNewPaste
-     * @function
-     */
-    me.stateOnlyNewPaste = function()
-    {
-        $message.text('');
-        $attachment.addClass('hidden');
-        $cloneButton.addClass('hidden');
-        $rawTextButton.addClass('hidden');
-        $remainingTime.addClass('hidden');
-        $pasteResult.addClass('hidden');
-        $clearText.addClass('hidden');
-        $discussion.addClass('hidden');
-        $prettyMessage.addClass('hidden');
-        $sendButton.addClass('hidden');
-        $expiration.addClass('hidden');
-        $formatter.addClass('hidden');
-        $burnAfterReadingOption.addClass('hidden');
-        $openDisc.addClass('hidden');
-        $password.addClass('hidden');
-        $attach.addClass('hidden');
-        $message.addClass('hidden');
-        $preview.addClass('hidden');
-        $loadingIndicator.addClass('hidden');
-
-        $newButton.removeClass('hidden');
-    };
+        /**
+         * clone the current paste
+         *
+         * @name   controller.clonePaste
+         * @function
+         * @param  {Event} event
+         */
+        me.clonePaste = function(event)
+        {
+            event.preventDefault();
+            me.stateNewPaste();
 
-    /**
-     * put the screen in "Existing paste" mode
-     *
-     * @name   controller.stateExistingPaste
-     * @function
-     * @param  {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false
-     */
-    me.stateExistingPaste = function(preview)
-    {
-        preview = preview || false;
+            // erase the id and the key in url
+            history.replaceState(null, document.title, helper.scriptLocation());
 
-        if (!preview)
-        {
-            // no "clone" for IE<10.
-            if ($('#oldienotice').is(":visible"))
+            me.showStatus('');
+            if ($attachmentLink.attr('href'))
             {
-                $cloneButton.addClass('hidden');
+                $clonedFile.removeClass('hidden');
+                $fileWrap.addClass('hidden');
             }
-            else
-            {
-                $cloneButton.removeClass('hidden');
-            }
-
-            $rawTextButton.removeClass('hidden');
-            $sendButton.addClass('hidden');
-            $attach.addClass('hidden');
-            $expiration.addClass('hidden');
-            $formatter.addClass('hidden');
-            $burnAfterReadingOption.addClass('hidden');
-            $openDisc.addClass('hidden');
-            $newButton.removeClass('hidden');
-            $preview.addClass('hidden');
-        }
-
-        $pasteResult.addClass('hidden');
-        $message.addClass('hidden');
-        $clearText.addClass('hidden');
-        $prettyMessage.addClass('hidden');
-        $loadingIndicator.addClass('hidden');
-    };
+            $message.text(
+                $('#pasteFormatter').val() === 'markdown' ?
+                    $prettyPrint.text() : $clearText.text()
+            );
+            $('.navbar-toggle').click();
+        };
 
-    /**
-     * when "burn after reading" is checked, disable discussion
-     *
-     * @name   controller.changeBurnAfterReading
-     * @function
-     */
-    me.changeBurnAfterReading = function()
-    {
-        if ($burnAfterReading.is(':checked') )
+        /**
+         * set the expiration on bootstrap templates
+         *
+         * @name   controller.setExpiration
+         * @function
+         * @param  {Event} event
+         */
+        me.setExpiration = function(event)
         {
-            $openDisc.addClass('buttondisabled');
-            $openDiscussion.attr({checked: false, disabled: true});
-        }
-        else
-        {
-            $openDisc.removeClass('buttondisabled');
-            $openDiscussion.removeAttr('disabled');
-        }
-    };
+            event.preventDefault();
+            var target = $(event.target);
+            $('#pasteExpiration').val(target.data('expiration'));
+            $('#pasteExpirationDisplay').text(target.text());
+        };
 
-    /**
-     * when discussion is checked, disable "burn after reading"
-     *
-     * @name   controller.changeOpenDisc
-     * @function
-     */
-    me.changeOpenDisc = function()
-    {
-        if ($openDiscussion.is(':checked') )
+        /**
+         * set the format on bootstrap templates
+         *
+         * @name   controller.setFormat
+         * @function
+         * @param  {Event} event
+         */
+        me.setFormat = function(event)
         {
-            $burnAfterReadingOption.addClass('buttondisabled');
-            $burnAfterReading.attr({checked: false, disabled: true});
-        }
-        else
-        {
-            $burnAfterReadingOption.removeClass('buttondisabled');
-            $burnAfterReading.removeAttr('disabled');
-        }
-    };
-
-    /**
-     * forward to URL shortener
-     *
-     * @name   controller.sendToShortener
-     * @function
-     * @param  {Event} event
-     */
-    me.sendToShortener = function(event)
-    {
-        window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl);
-        event.preventDefault();
-    };
-
-    /**
-     * reload the page
-     *
-     * This takes the user to the PrivateBin home page.
-     *
-     * @name   controller.reloadPage
-     * @function
-     * @param  {Event} event
-     */
-    me.reloadPage = function(event)
-    {
-        window.location.href = helper.scriptLocation();
-        event.preventDefault();
-    };
+            var target = $(event.target);
+            $('#pasteFormatter').val(target.data('format'));
+            $('#pasteFormatterDisplay').text(target.text());
 
-    /**
-     * return raw text
-     *
-     * @name   controller.rawText
-     * @function
-     * @param  {Event} event
-     */
-    me.rawText = function(event)
-    {
-        var paste = $('#pasteFormatter').val() === 'markdown' ?
-            $prettyPrint.text() : $clearText.text();
-        history.pushState(
-            null, document.title, helper.scriptLocation() + '?' +
-            helper.pasteId() + '#' + helper.pageKey()
-        );
-        // we use text/html instead of text/plain to avoid a bug when
-        // reloading the raw text view (it reverts to type text/html)
-        var newDoc = document.open('text/html', 'replace');
-        newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
-        newDoc.close();
-
-        event.preventDefault();
-    };
-
-    /**
-     * clone the current paste
-     *
-     * @name   controller.clonePaste
-     * @function
-     * @param  {Event} event
-     */
-    me.clonePaste = function(event)
-    {
-        event.preventDefault();
-        me.stateNewPaste();
-
-        // erase the id and the key in url
-        history.replaceState(null, document.title, helper.scriptLocation());
+            if ($messagePreview.parent().hasClass('active')) {
+                me.viewPreview(event);
+            }
+            event.preventDefault();
+        };
 
-        me.showStatus('');
-        if ($attachmentLink.attr('href'))
+        /**
+         * set the language in a cookie and reload the page
+         *
+         * @name   controller.setLanguage
+         * @function
+         * @param  {Event} event
+         */
+        me.setLanguage = function(event)
         {
-            $clonedFile.removeClass('hidden');
-            $fileWrap.addClass('hidden');
-        }
-        $message.text(
-            $('#pasteFormatter').val() === 'markdown' ?
-                $prettyPrint.text() : $clearText.text()
-        );
-        $('.navbar-toggle').click();
-    };
-
-    /**
-     * set the expiration on bootstrap templates
-     *
-     * @name   controller.setExpiration
-     * @function
-     * @param  {Event} event
-     */
-    me.setExpiration = function(event)
-    {
-        event.preventDefault();
-        var target = $(event.target);
-        $('#pasteExpiration').val(target.data('expiration'));
-        $('#pasteExpirationDisplay').text(target.text());
-    };
-
-    /**
-     * set the format on bootstrap templates
-     *
-     * @name   controller.setFormat
-     * @function
-     * @param  {Event} event
-     */
-    me.setFormat = function(event)
-    {
-        var target = $(event.target);
-        $('#pasteFormatter').val(target.data('format'));
-        $('#pasteFormatterDisplay').text(target.text());
-
-        if ($messagePreview.parent().hasClass('active')) {
-            me.viewPreview(event);
-        }
-        event.preventDefault();
-    };
+            document.cookie = 'lang=' + $(event.target).data('lang');
+            me.reloadPage(event);
+        };
 
-    /**
-     * set the language in a cookie and reload the page
-     *
-     * @name   controller.setLanguage
-     * @function
-     * @param  {Event} event
-     */
-    me.setLanguage = function(event)
-    {
-        document.cookie = 'lang=' + $(event.target).data('lang');
-        me.reloadPage(event);
-    };
+        /**
+         * support input of tab character
+         *
+         * @name   controller.supportTabs
+         * @function
+         * @param  {Event} event
+         * @TODO doc what is @this here?
+         */
+        me.supportTabs = function(event)
+        {
+            var keyCode = event.keyCode || event.which;
+            // tab was pressed
+            if (keyCode === 9)
+            {
+                // prevent the textarea to lose focus
+                event.preventDefault();
+                // get caret position & selection
+                var val   = this.value,
+                    start = this.selectionStart,
+                    end   = this.selectionEnd;
+                // set textarea value to: text before caret + tab + text after caret
+                this.value = val.substring(0, start) + '\t' + val.substring(end);
+                // put caret at right position again
+                this.selectionStart = this.selectionEnd = start + 1;
+            }
+        };
 
-    /**
-     * support input of tab character
-     *
-     * @name   controller.supportTabs
-     * @function
-     * @param  {Event} event
-     * @TODO doc what is @this here?
-     */
-    me.supportTabs = function(event)
-    {
-        var keyCode = event.keyCode || event.which;
-        // tab was pressed
-        if (keyCode === 9)
+        /**
+         * view the editor tab
+         *
+         * @name   controller.viewEditor
+         * @function
+         * @param  {Event} event
+         */
+        me.viewEditor = function(event)
         {
-            // prevent the textarea to lose focus
-            event.preventDefault();
-            // get caret position & selection
-            var val   = this.value,
-                start = this.selectionStart,
-                end   = this.selectionEnd;
-            // set textarea value to: text before caret + tab + text after caret
-            this.value = val.substring(0, start) + '\t' + val.substring(end);
-            // put caret at right position again
-            this.selectionStart = this.selectionEnd = start + 1;
-        }
-    };
+            $messagePreview.parent().removeClass('active');
+            $messageEdit.parent().addClass('active');
+            $message.focus();
+            me.stateNewPaste();
 
-    /**
-     * view the editor tab
-     *
-     * @name   controller.viewEditor
-     * @function
-     * @param  {Event} event
-     */
-    me.viewEditor = function(event)
-    {
-        $messagePreview.parent().removeClass('active');
-        $messageEdit.parent().addClass('active');
-        $message.focus();
-        me.stateNewPaste();
-
-        event.preventDefault();
-    };
+            event.preventDefault();
+        };
 
-    /**
-     * view the preview tab
-     *
-     * @name   controller.viewPreview
-     * @function
-     * @param  {Event} event
-     */
-    me.viewPreview = function(event)
-    {
-        $messageEdit.parent().removeClass('active');
-        $messagePreview.parent().addClass('active');
-        $message.focus();
-        me.stateExistingPaste(true);
-        me.formatPaste($('#pasteFormatter').val(), $message.val());
-
-        event.preventDefault();
-    };
+        /**
+         * view the preview tab
+         *
+         * @name   controller.viewPreview
+         * @function
+         * @param  {Event} event
+         */
+        me.viewPreview = function(event)
+        {
+            $messageEdit.parent().removeClass('active');
+            $messagePreview.parent().addClass('active');
+            $message.focus();
+            me.stateExistingPaste(true);
+            me.formatPaste($('#pasteFormatter').val(), $message.val());
 
-    /**
-     * handle history (pop) state changes
-     *
-     * currently this does only handle redirects to the home page.
-     *
-     * @name   controller.historyChange
-     * @function
-     * @param  {Event} event
-     */
-    me.historyChange = function(event)
-    {
-        var currentLocation = helper.scriptLocation();
-        if (event.originalEvent.state === null && // no state object passed
-            event.originalEvent.target.location.href === currentLocation && // target location is home page
-            window.location.href === currentLocation // and we are not already on the home page
-        ) {
-            // redirect to home page
-            window.location.href = currentLocation;
-        }
-    };
+            event.preventDefault();
+        };
 
-    /**
-     * 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
-     * @function
-     * @param  {Event} event
-     */
-    me.pasteLinkClick = function(event)
-    {
-        // check if location is (already) shown in URL bar
-        if (window.location.href === $pasteUrl.attr('href')) {
-            // if so we need to load link by reloading the current site
-            window.location.reload(true);
-        }
-    };
+        /**
+         * handle history (pop) state changes
+         *
+         * currently this does only handle redirects to the home page.
+         *
+         * @name   controller.historyChange
+         * @function
+         * @param  {Event} event
+         */
+        me.historyChange = function(event)
+        {
+            var currentLocation = helper.scriptLocation();
+            if (event.originalEvent.state === null && // no state object passed
+                event.originalEvent.target.location.href === currentLocation && // target location is home page
+                window.location.href === currentLocation // and we are not already on the home page
+            ) {
+                // redirect to home page
+                window.location.href = currentLocation;
+            }
+        };
 
-    /**
-     * create a new paste
-     *
-     * @name   controller.newPaste
-     * @function
-     */
-    me.newPaste = function()
-    {
-        me.stateNewPaste();
-        me.showStatus('');
-        $message.text('');
-        me.changeBurnAfterReading();
-        me.changeOpenDisc();
-    };
+        /**
+         * 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
+         * @function
+         * @param  {Event} event
+         */
+        me.pasteLinkClick = function(event)
+        {
+            // check if location is (already) shown in URL bar
+            if (window.location.href === $pasteUrl.attr('href')) {
+                // if so we need to load link by reloading the current site
+                window.location.reload(true);
+            }
+        };
 
-    /**
-     * removes an attachment
-     *
-     * @name   controller.removeAttachment
-     * @function
-     */
-    me.removeAttachment = 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');
-    };
+        /**
+         * create a new paste
+         *
+         * @name   controller.newPaste
+         * @function
+         */
+        me.newPaste = function()
+        {
+            me.stateNewPaste();
+            me.showStatus('');
+            $message.text('');
+            me.changeBurnAfterReading();
+            me.changeOpenDisc();
+        };
 
-    /**
-     * decrypt using the password from the modal dialog
-     *
-     * @name   controller.decryptPasswordModal
-     * @function
-     */
-    me.decryptPasswordModal = function()
-    {
-        $passwordInput.val($passwordDecrypt.val());
-        me.displayMessages();
-    };
+        /**
+         * removes an attachment
+         *
+         * @name   controller.removeAttachment
+         * @function
+         */
+        me.removeAttachment = 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');
+        };
 
-    /**
-     * submit a password in the modal dialog
-     *
-     * @name   controller.submitPasswordModal
-     * @function
-     * @param  {Event} event
-     */
-    me.submitPasswordModal = function(event)
-    {
-        event.preventDefault();
-        $passwordModal.modal('hide');
-    };
+        /**
+         * decrypt using the password from the modal dialog
+         *
+         * @name   controller.decryptPasswordModal
+         * @function
+         */
+        me.decryptPasswordModal = function()
+        {
+            $passwordInput.val($passwordDecrypt.val());
+            me.displayMessages();
+        };
 
-    /**
-     * display an error message,
-     * we use the same function for paste and reply to comments
-     *
-     * @name   controller.showError
-     * @function
-     * @param  {string} message - text to display
-     */
-    me.showError = function(message)
-    {
-        if ($status.length)
+        /**
+         * submit a password in the modal dialog
+         *
+         * @name   controller.submitPasswordModal
+         * @function
+         * @param  {Event} event
+         */
+        me.submitPasswordModal = function(event)
         {
-            $status.addClass('errorMessage').text(message);
-        }
-        else
+            event.preventDefault();
+            $passwordModal.modal('hide');
+        };
+
+        /**
+         * display an error message,
+         * we use the same function for paste and reply to comments
+         *
+         * @name   controller.showError
+         * @function
+         * @param  {string} message - text to display
+         */
+        me.showError = function(message)
         {
-            $errorMessage.removeClass('hidden');
-            helper.setMessage($errorMessage, message);
-        }
-        if (typeof $replyStatus !== 'undefined') {
-            $replyStatus.addClass('errorMessage');
-            $replyStatus.addClass($errorMessage.attr('class'));
             if ($status.length)
             {
-                $replyStatus.html($status.html());
+                $status.addClass('errorMessage').text(message);
             }
             else
             {
-                $replyStatus.html($errorMessage.html());
+                $errorMessage.removeClass('hidden');
+                helper.setMessage($errorMessage, message);
             }
-        }
-    };
+            if (typeof $replyStatus !== 'undefined') {
+                $replyStatus.addClass('errorMessage');
+                $replyStatus.addClass($errorMessage.attr('class'));
+                if ($status.length)
+                {
+                    $replyStatus.html($status.html());
+                }
+                else
+                {
+                    $replyStatus.html($errorMessage.html());
+                }
+            }
+        };
 
-    /**
-     * display a status message,
-     * we use the same function for paste and reply 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.showStatus = function(message, spin)
-    {
-        if (spin || false)
+        /**
+         * display a status message,
+         * we use the same function for paste and reply 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.showStatus = function(message, spin)
         {
-            var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
-            $status.prepend(img);
+            if (spin || false)
+            {
+                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
+                $status.prepend(img);
+                if (typeof $replyStatus !== 'undefined') {
+                    $replyStatus.prepend(img);
+                }
+            }
             if (typeof $replyStatus !== 'undefined') {
-                $replyStatus.prepend(img);
+                $replyStatus.removeClass('errorMessage').text(message);
             }
-        }
-        if (typeof $replyStatus !== 'undefined') {
-            $replyStatus.removeClass('errorMessage').text(message);
-        }
-        if (!message)
-        {
-            $status.html(' ');
-            return;
-        }
-        if (message === '')
-        {
-            $status.html(' ');
-            return;
-        }
-        $status.removeClass('errorMessage').text(message);
-    };
-
-    /**
-     * bind events to DOM elements
-     *
-     * @private
-     * @function
-     */
-    function bindEvents()
-    {
-        $burnAfterReading.change(me.changeBurnAfterReading);
-        $openDisc.change(me.changeOpenDisc);
-        $sendButton.click(me.sendData);
-        $cloneButton.click(me.clonePaste);
-        $rawTextButton.click(me.rawText);
-        $fileRemoveButton.click(me.removeAttachment);
-        $('.reloadlink').click(me.reloadPage);
-        $message.keydown(me.supportTabs);
-        $messageEdit.click(me.viewEditor);
-        $messagePreview.click(me.viewPreview);
-
-        // bootstrap template drop downs
-        $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration);
-        $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat);
-        $('#language ul.dropdown-menu li a').click(me.setLanguage);
-
-        // page template drop down
-        $('#language select option').click(me.setLanguage);
-
-        // focus password input when it is shown
-        $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);
-
-        $(window).on('popstate', me.historyChange);
-    }
+            if (!message)
+            {
+                $status.html(' ');
+                return;
+            }
+            if (message === '')
+            {
+                $status.html(' ');
+                return;
+            }
+            $status.removeClass('errorMessage').text(message);
+        };
 
-    /**
-     * main application
-     *
-     * @name   controller.init
-     * @function
-     */
-    me.init = function()
-    {
-        // hide "no javascript" message
-        $('#noscript').hide();
-
-        // preload jQuery wrapped DOM elements and bind events
-        $attach = $('#attach');
-        $attachment = $('#attachment');
-        $attachmentLink = $('#attachment a');
-        $burnAfterReading = $('#burnafterreading');
-        $burnAfterReadingOption = $('#burnafterreadingoption');
-        $cipherData = $('#cipherdata');
-        $clearText = $('#cleartext');
-        $cloneButton = $('#clonebutton');
-        $clonedFile = $('#clonedfile');
-        $comments = $('#comments');
-        $discussion = $('#discussion');
-        $errorMessage = $('#errormessage');
-        $expiration = $('#expiration');
-        $fileRemoveButton = $('#fileremovebutton');
-        $fileWrap = $('#filewrap');
-        $formatter = $('#formatter');
-        $image = $('#image');
-        $loadingIndicator = $('#loadingindicator');
-        $message = $('#message');
-        $messageEdit = $('#messageedit');
-        $messagePreview = $('#messagepreview');
-        $newButton = $('#newbutton');
-        $openDisc = $('#opendisc');
-        $openDiscussion = $('#opendiscussion');
-        $password = $('#password');
-        $passwordInput = $('#passwordinput');
-        $passwordModal = $('#passwordmodal');
-        $passwordForm = $('#passwordform');
-        $passwordDecrypt = $('#passworddecrypt');
-        $pasteResult = $('#pasteresult');
-        // $pasteUrl is saved in sendDataContinue() if/after it is
-        // actually created
-        $prettyMessage = $('#prettymessage');
-        $prettyPrint = $('#prettyprint');
-        $preview = $('#preview');
-        $rawTextButton = $('#rawtextbutton');
-        $remainingTime = $('#remainingtime');
-        // $replyStatus is saved in openReply()
-        $sendButton = $('#sendbutton');
-        $status = $('#status');
-        bindEvents();
-
-        // display status returned by php code, if any (eg. paste was properly deleted)
-        if ($status.text().length > 0)
+        /**
+         * bind events to DOM elements
+         *
+         * @private
+         * @function
+         */
+        function bindEvents()
         {
-            me.showStatus($status.text());
-            return;
-        }
+            $burnAfterReading.change(me.changeBurnAfterReading);
+            $openDisc.change(me.changeOpenDisc);
+            $sendButton.click(me.sendData);
+            $cloneButton.click(me.clonePaste);
+            $rawTextButton.click(me.rawText);
+            $fileRemoveButton.click(me.removeAttachment);
+            $('.reloadlink').click(me.reloadPage);
+            $message.keydown(me.supportTabs);
+            $messageEdit.click(me.viewEditor);
+            $messagePreview.click(me.viewPreview);
+
+            // bootstrap template drop downs
+            $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration);
+            $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat);
+            $('#language ul.dropdown-menu li a').click(me.setLanguage);
+
+            // page template drop down
+            $('#language select option').click(me.setLanguage);
+
+            // focus password input when it is shown
+            $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);
 
-        // keep line height even if content empty
-        $status.html(' ');
+            $(window).on('popstate', me.historyChange);
+        };
 
-        // display an existing paste
-        if ($cipherData.text().length > 1)
+        /**
+         * main application
+         *
+         * @name   controller.init
+         * @function
+         */
+        me.init = function()
         {
-            // missing decryption key in URL?
-            if (window.location.hash.length === 0)
+            // hide "no javascript" message
+            $('#noscript').hide();
+
+            // preload jQuery wrapped DOM elements and bind events
+            $attach = $('#attach');
+            $attachment = $('#attachment');
+            $attachmentLink = $('#attachment a');
+            $burnAfterReading = $('#burnafterreading');
+            $burnAfterReadingOption = $('#burnafterreadingoption');
+            $cipherData = $('#cipherdata');
+            $clearText = $('#cleartext');
+            $cloneButton = $('#clonebutton');
+            $clonedFile = $('#clonedfile');
+            $comments = $('#comments');
+            $discussion = $('#discussion');
+            $errorMessage = $('#errormessage');
+            $expiration = $('#expiration');
+            $fileRemoveButton = $('#fileremovebutton');
+            $fileWrap = $('#filewrap');
+            $formatter = $('#formatter');
+            $image = $('#image');
+            $loadingIndicator = $('#loadingindicator');
+            $message = $('#message');
+            $messageEdit = $('#messageedit');
+            $messagePreview = $('#messagepreview');
+            $newButton = $('#newbutton');
+            $openDisc = $('#opendisc');
+            $openDiscussion = $('#opendiscussion');
+            $password = $('#password');
+            $passwordInput = $('#passwordinput');
+            $passwordModal = $('#passwordmodal');
+            $passwordForm = $('#passwordform');
+            $passwordDecrypt = $('#passworddecrypt');
+            $pasteResult = $('#pasteresult');
+            // $pasteUrl is saved in sendDataContinue() if/after it is
+            // actually created
+            $prettyMessage = $('#prettymessage');
+            $prettyPrint = $('#prettyprint');
+            $preview = $('#preview');
+            $rawTextButton = $('#rawtextbutton');
+            $remainingTime = $('#remainingtime');
+            // $replyStatus is saved in openReply()
+            $sendButton = $('#sendbutton');
+            $status = $('#status');
+            bindEvents();
+
+            // display status returned by php code, if any (eg. paste was properly deleted)
+            if ($status.text().length > 0)
             {
-                me.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?)'));
+                me.showStatus($status.text());
                 return;
             }
 
-            // show proper elements on screen
-            me.stateExistingPaste();
-            me.displayMessages();
-        }
-        // display error message from php code
-        else if ($errorMessage.text().length > 1)
-        {
-            me.showError($errorMessage.text());
-        }
-        // create a new paste
-        else
-        {
-            me.newPaste();
-        }
-    };
+            // keep line height even if content empty
+            $status.html(' ');
 
-    return me;
-})(window, document, jQuery, sjcl, Base64, RawDeflate);
+            // display an existing paste
+            if ($cipherData.text().length > 1)
+            {
+                // missing decryption key in URL?
+                if (window.location.hash.length === 0)
+                {
+                    me.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
+                me.stateExistingPaste();
+                me.displayMessages();
+            }
+            // display error message from php code
+            else if ($errorMessage.text().length > 1)
+            {
+                me.showError($errorMessage.text());
+            }
+            // create a new paste
+            else
+            {
+                me.newPaste();
+            }
+        };
+
+        return me;
+    })(window, document);
+
+    /**
+     * main application start, called when DOM is fully loaded and
+     * runs controller initalization after translations are loaded
+     */
+    $(i18n.loadTranslations);
+
+    return {
+        helper: helper,
+        i18n: i18n,
+        filter: filter,
+        controller: controller
+    };
+}(jQuery, sjcl, Base64, RawDeflate);