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

refactored JS:
- moved from global namespace into anonymous function
- as onclick has no direct access to the zerobin methods, the events are
now bound in the init() method via bindEvents()
- to simplify maintenance, the functions were wrapped into 3 objects:
zerobin (display logic, event handling), filter (compression,
encryption) and helper (stateless utilities)
- some CSS and template adjustements were also done

El RIDO 11 лет назад
Родитель
Сommit
3099e10dfa
5 измененных файлов с 884 добавлено и 624 удалено
  1. 10 2
      css/bootstrap/zerobin.css
  2. 0 1
      css/zerobin.css
  3. 863 610
      js/zerobin.js
  4. 5 5
      tpl/bootstrap.html
  5. 6 6
      tpl/page.html

+ 10 - 2
css/bootstrap/zerobin.css

@@ -21,12 +21,20 @@ body {
 	margin: 5px 0;
 }
 
+#comments, #comments button {
+	margin-bottom: 10px;
+}
+
 .comment {
 	border-left: 1px solid #ccc;
-	padding: 5px 0 5px 5px;
+	padding: 5px 0 5px 10px;
 	white-space: pre-wrap;
 }
 
-h4 {
+footer h4 {
 	margin-top: 0;
 }
+
+li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 {
+	list-style-type: decimal !important;
+}

+ 0 - 1
css/zerobin.css

@@ -65,7 +65,6 @@ h3 {
 }   
 
 #aboutbox { 
-	font-size: 1.2em;
 	color: #94a3b4; 
 	padding: 4px 8px 4px 16px;
 	position: relative; 

+ 863 - 610
js/zerobin.js

@@ -12,651 +12,904 @@
 // Immediately start random number generator collector.
 sjcl.random.startCollectors();
 
-/**
- * Converts a duration (in seconds) into human readable format.
- *
- * @param int seconds
- * @return string
- */
-function secondsToHuman(seconds)
-{
-    if (seconds<60) { var v=Math.floor(seconds); return v+' second'+((v>1)?'s':''); }
-    if (seconds<60*60) { var v=Math.floor(seconds/60); return v+' minute'+((v>1)?'s':''); }
-    if (seconds<60*60*24) { var v=Math.floor(seconds/(60*60)); return v+' hour'+((v>1)?'s':''); }
-    // If less than 2 months, display in days:
-    if (seconds<60*60*24*60) { var v=Math.floor(seconds/(60*60*24)); return v+' day'+((v>1)?'s':''); }
-    var v=Math.floor(seconds/(60*60*24*30)); return v+' month'+((v>1)?'s':'');
-}
-
-/**
- * Converts an associative array to an encoded string
- * for appending to the anchor.
- *
- * @param object associative_array Object to be serialized
- * @return string
- */
-function hashToParameterString(associativeArray)
-{
-  var parameterString = "";
-  for (key in associativeArray)
-  {
-    if( parameterString === "" )
-    {
-      parameterString = encodeURIComponent(key);
-      parameterString += "=" + encodeURIComponent(associativeArray[key]);
-    } else {
-      parameterString += "&" + encodeURIComponent(key);
-      parameterString += "=" + encodeURIComponent(associativeArray[key]);
-    }
-  }
-  //padding for URL shorteners
-  parameterString += "&p=p";
-
-  return parameterString;
-}
-
-/**
- * Converts a string to an associative array.
- *
- * @param string parameter_string String containing parameters
- * @return object
- */
-function parameterStringToHash(parameterString)
-{
-  var parameterHash = {};
-  var parameterArray = parameterString.split("&");
-  for (var i = 0; i < parameterArray.length; i++) {
-    //var currentParamterString = decodeURIComponent(parameterArray[i]);
-    var pair = parameterArray[i].split("=");
-    var key = decodeURIComponent(pair[0]);
-    var value = decodeURIComponent(pair[1]);
-    parameterHash[key] = value;
-  }
-
-  return parameterHash;
-}
-
-/**
- * Get an associative array of the parameters found in the anchor
- *
- * @return object
- */
-function getParameterHash()
-{
-  var hashIndex = window.location.href.indexOf("#");
-  if (hashIndex >= 0) {
-    return parameterStringToHash(window.location.href.substring(hashIndex + 1));
-  } else {
-    return {};
-  }
-}
-
-/**
- * Compress a message (deflate compression). Returns base64 encoded data.
- *
- * @param string message
- * @return base64 string data
- */
-function compress(message) {
-    return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
-}
-
-/**
- * Decompress a message compressed with compress().
- */
-function decompress(data) {
-    return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
-}
+$(function(){
+    'use strict';
+    
+    /**
+     * static helper methods
+     */
+    var helper = {
+
+        /**
+         * Converts a duration (in seconds) into human readable format.
+         *
+         * @param int seconds
+         * @return string
+         */
+        secondsToHuman: function(seconds)
+        {
+            if (seconds < 60)
+            {
+                var v = Math.floor(seconds);
+                return v + ' second' + ((v > 1) ? 's' : '');
+            }
+            if (seconds < 60 * 60)
+            {
+                var v = Math.floor(seconds / 60);
+                return v + ' minute' + ((v > 1) ? 's' : '');
+            }
+            if (seconds < 60 * 60 * 24)
+            {
+                var v = Math.floor(seconds / (60 * 60));
+                return v + ' hour' + ((v > 1) ? 's' : '');
+            }
+            // If less than 2 months, display in days:
+            if (seconds < 60 * 60 * 24 * 60)
+            {
+                var v = Math.floor(seconds / (60 * 60 * 24));
+                return v + ' day' + ((v > 1) ? 's' : '');
+            }
+            var v = Math.floor(seconds / (60 * 60 * 24 * 30));
+            return v + ' month' + ((v > 1) ? 's' : '');
+        },
+
+        /**
+         * Converts an associative array to an encoded string
+         * for appending to the anchor.
+         *
+         * @param object associative_array Object to be serialized
+         * @return string
+         */
+        hashToParameterString: function(associativeArray)
+        {
+            var parameterString = '';
+            for (key in associativeArray)
+            {
+                if(parameterString === '')
+                {
+                    parameterString = encodeURIComponent(key);
+                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
+                }
+                else
+                {
+                    parameterString += '&' + encodeURIComponent(key);
+                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
+                }
+            }
+            // padding for URL shorteners
+            parameterString += '&p=p';
+
+            return parameterString;
+        },
+
+        /**
+         * Converts a string to an associative array.
+         *
+         * @param string parameter_string String containing parameters
+         * @return object
+         */
+        parameterStringToHash: function(parameterString)
+        {
+            var parameterHash = {};
+            var parameterArray = parameterString.split('&');
+            for (var i = 0; i < parameterArray.length; i++)
+            {
+                var pair = parameterArray[i].split('=');
+                var key = decodeURIComponent(pair[0]);
+                var value = decodeURIComponent(pair[1]);
+                parameterHash[key] = value;
+            }
 
-/**
- * Compress, then encrypt message with key.
- *
- * @param string key
- * @param string message
- * @return encrypted string data
- */
-function zeroCipher(key, message) {
-    if ($('#passwordinput').val().length == 0) {
-        return sjcl.encrypt(key, compress(message));
-    }
-    return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash($("#passwordinput").val())), compress(message));
-}
+            return parameterHash;
+        },
 
-/**
- * Decrypt message with key, then decompress.
- *
- * @param string key
- * @param encrypted string data
- * @return string readable message
- */
-function zeroDecipher(key, data) {
-    if (data != undefined) {
-        try {
-            return decompress(sjcl.decrypt(key, data));
-        } catch (err) {
-            try {
-                if ($('#passwordinput').val().length > 0) {
-                    password = $('#passwordinput').val();
-                } else {
-                    password = prompt("Please enter the password for this paste:", "");
-                    if (password == null) return null;
+        /**
+         * Get an associative array of the parameters found in the anchor
+         *
+         * @return object
+         */
+        getParameterHash: function()
+        {
+            var hashIndex = window.location.href.indexOf('#');
+            if (hashIndex >= 0)
+            {
+                return this.parameterStringToHash(window.location.href.substring(hashIndex + 1));
+            }
+            else
+            {
+                return {};
+            }
+        },
+
+        /**
+         * Convert all applicable characters to HTML entities
+         *
+         * @param string str
+         * @returns string encoded string
+         */
+        htmlEntities: function(str)
+        {
+            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+        },
+
+        /**
+         * Text range selection.
+         * From: http://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
+         *
+         * @param string element : Indentifier of the element to select (id="").
+         */
+        selectText: function(element)
+        {
+            var doc = document,
+                text = doc.getElementById(element),
+                range,
+                selection;
+
+            // MS
+            if (doc.body.createTextRange)
+            {
+                range = doc.body.createTextRange();
+                range.moveToElementText(text);
+                range.select();
+            }
+            // all others
+            else if (window.getSelection)
+            {
+                selection = window.getSelection();
+                range = doc.createRange();
+                range.selectNodeContents(text);
+                selection.removeAllRanges();
+                selection.addRange(range);
+            }
+        },
+
+        /**
+         * Set text of a DOM element (required for IE)
+         * This is equivalent to element.text(text)
+         *
+         * @param object element : a DOM element.
+         * @param string text : the text to enter.
+         */
+        setElementText: function(element, text)
+        {
+            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
+            if ($('#oldienotice').is(':visible')) {
+                var html = this.htmlEntities(text).replace(/\n/ig,'\r\n<br>');
+                element.html('<pre>'+html+'</pre>');
+            }
+            // for other (sane) browsers:
+            else
+            {
+                element.text(text);
+            }
+        },
+
+        /**
+         * Convert URLs to clickable links.
+         * URLs to handle:
+         * <code>
+         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
+         *     http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         *     http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         * </code>
+         *
+         * @param object element : a jQuery DOM element.
+         */
+        urls2links: function(element)
+        {
+            element.html(
+                element.html().replace(
+                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
+                    '<a href="$1" rel="nofollow">$1</a>'
+                )
+            );
+            element.html(
+                element.html().replace(
+                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
+                    '<a href="$1">$1</a>'
+                )
+            );
+        }
+    };
+
+    /**
+     * filter methods
+     */
+    var filter = {
+        /**
+         * Compress a message (deflate compression). Returns base64 encoded data.
+         *
+         * @param string message
+         * @return base64 string data
+         */
+        compress: function(message)
+        {
+            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
+        },
+
+        /**
+         * Decompress a message compressed with compress().
+         *
+         * @param base64 string data
+         * @return string message
+         */
+        decompress: function(data)
+        {
+            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
+        },
+
+        /**
+         * Compress, then encrypt message with key.
+         *
+         * @param string key
+         * @param string password
+         * @param string message
+         * @return encrypted string data
+         */
+        cipher: function(key, password, message)
+        {
+            password = password.trim();
+            if (password.length == 0)
+            {
+                return sjcl.encrypt(key, this.compress(message));
+            }
+            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.compress(message));
+        },
+
+        /**
+         * Decrypt message with key, then decompress.
+         *
+         * @param string key
+         * @param string password
+         * @param encrypted string data
+         * @return string readable message
+         */
+        decipher: function(key, password, data)
+        {
+            if (data != undefined)
+            {
+                try
+                {
+                    return this.decompress(sjcl.decrypt(key, data));
+                }
+                catch(err)
+                {
+                    try
+                    {
+                        return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
+                    }
+                    catch(err)
+                    {}
                 }
-                data = decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
-                $('#passwordinput').val(password);
-                return data;
-            } catch (err) {
-                return zeroDecipher(key, data);
             }
+            return '';
         }
-    }
-}
+    };
+
+    var zerobin = {
+        /**
+         * Get the current script location (without search or hash part of the URL).
+         * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
+         *
+         * @return string current script location
+         */
+        scriptLocation: function()
+        {
+            var 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)
+            {
+                scriptLocation = scriptLocation.substring(0, hashIndex);
+            }
+            return scriptLocation;
+        },
+
+        /**
+         * Get the pastes unique identifier from the URL
+         * eg. http://server.com/zero/?c05354954c49a487#xxx --> c05354954c49a487
+         *
+         * @return string unique identifier
+         */
+        pasteID: function()
+        {
+            return window.location.search.substring(1);
+        },
+
+        /**
+         * Return the deciphering key stored in anchor part of the URL
+         *
+         * @return string key
+         */
+        pageKey: function()
+        {
+            var key = window.location.hash.substring(1);  // Get key
 
-/**
- * Get the current script location (without search or hash part of the URL).
- * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
- * 
- * @return string current script location
- */
-function scriptLocation() {
-  var 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) {
-    scriptLocation = scriptLocation.substring(0, hashIndex);
-  }
-  return scriptLocation;
-}
+            // Some stupid web 2.0 services and redirectors add data AFTER the anchor
+            // (such as &utm_source=...).
+            // We will strip any additional data.
 
-/**
- * Get the pastes unique identifier from the URL
- * eg. http://server.com/zero/?c05354954c49a487#xxx --> c05354954c49a487
- * 
- * @return string unique identifier
- */
-function pasteID() {
-    return window.location.search.substring(1);
-}
+            // First, strip everything after the equal sign (=) which signals end of base64 string.
+            var i = key.indexOf('=');
+            if (i > -1)
+            {
+                key = key.substring(0, i + 1);
+            }
 
-/**
- * Convert all applicable characters to HTML entities
- * 
- * @param string str
- * @returns string encoded string
- */
-function htmlEntities(str) {
-    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
-}
-/**
- * Set text of a DOM element (required for IE)
- * This is equivalent to element.text(text)
- * 
- * @param object element : a DOM element.
- * @param string text : the text to enter.
- */
-function setElementText(element, text) {
-    // For IE<10.
-    if ($('#oldienotice').is(":visible")) {
-        // IE<10 does not support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
-        var html = htmlEntities(text).replace(/\n/ig,"\r\n<br>");
-        element.html('<pre>'+html+'</pre>');
-    }
-    // for other (sane) browsers:
-    else {
-        element.text(text);
-    }
-}
+            // If the equal sign was not present, some parameters may remain:
+            i = key.indexOf('&');
+            if (i > -1)
+            {
+                key = key.substring(0, i);
+            }
 
-/**
- * Show decrypted text in the display area, including discussion (if open)
- *
- * @param string key : decryption key
- * @param array comments : Array of messages to display (items = array with keys ('data','meta')
- */
-function displayMessages(key, comments) {
-    try { // Try to decrypt the paste.
-        var cleartext = zeroDecipher(key, comments[0].data);
-        if (cleartext == null) throw "password prompt canceled";
-    } catch(err) {
-        $('#cleartext').addClass('hidden');
-        $('#prettymessage').addClass('hidden');
-        $('#clonebutton').addClass('hidden');
-        showError('Could not decrypt data (Wrong key ?)');
-        return;
-    }
-    setElementText($('#cleartext'), cleartext);
-    setElementText($('#prettyprint'), cleartext);
-    // Convert URLs to clickable links.
-    urls2links($('#cleartext'));
-    urls2links($('#prettyprint'));
-    if (typeof prettyPrint == 'function') prettyPrint();
-
-    // Display paste expiration.
-    if (comments[0].meta.expire_date) $('#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').removeClass('hidden');
-    if (comments[0].meta.burnafterreading) {
-        $.get(scriptLocation() + "?pasteid=" + pasteID() + '&deletetoken=burnafterreading', 'json')
-        .fail(function() {
-            showError('Could not delete the paste, it was not stored in burn after reading mode.');
-        });
-        $('#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY.  Don\'t close this window, this message can\'t be displayed again.').removeClass('hidden');
-        $('#clonebutton').addClass('hidden'); // Discourage cloning (as it can't really be prevented).
-    }
+            // Then add trailing equal sign if it's missing
+            if (key.charAt(key.length - 1) !== '=') key += '=';
 
-    // If the discussion is opened on this paste, display it.
-    if (comments[0].meta.opendiscussion) {
-        $('#comments').html('');
-        // iterate over comments
-        for (var i = 1; i < comments.length; i++) {
-            var comment=comments[i];
-            var cleartext="[Could not decrypt comment ; Wrong key ?]";
-            try {
-                cleartext = zeroDecipher(key, comment.data);
-            } catch(err) { }
-            var place = $('#comments');
-            // If parent comment exists, display below (CSS will automatically shift it right.)
-            var cname = '#comment_'+comment.meta.parentid;
-
-            // If the element exists in page
-            if ($(cname).length) {
-                place = $(cname);
-            }
-            var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
-                               + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
-                               + '<button onclick="open_reply($(this),\'' + comment.meta.commentid + '\');return false;" class="btn btn-default">Reply</button>'
-                               + '</div></article>');
-            setElementText(divComment.find('div.commentdata'), cleartext);
-            // Convert URLs to clickable links in comment.
-            urls2links(divComment.find('div.commentdata'));
-            divComment.find('span.nickname').html('<i>(Anonymous)</i>');
-
-            // Try to get optional nickname:
-            try {
-                divComment.find('span.nickname').text(zeroDecipher(key, comment.meta.nickname));
-            } catch(err) { }
-            divComment.find('span.commentdate').text('  ('+(new Date(comment.meta.postdate*1000).toString())+')').attr('title','CommentID: ' + comment.meta.commentid);
-
-            // If an avatar is available, display it.
-            if (comment.meta.vizhash) {
-                divComment.find('span.nickname').before('<img src="' + comment.meta.vizhash + '" class="vizhash" title="Anonymous avatar (Vizhash of the IP address)" /> ');
-            }
-
-            place.append(divComment);
-        }
-        $('#comments').append('<div class="comment"><button onclick="open_reply($(this),\'' + pasteID() + '\');return false;" class="btn btn-default">Add comment</button></div>');
-        $('#discussion').removeClass('hidden');
-    }
-}
+            return key;
+        },
 
-/**
- * Open the comment entry when clicking the "Reply" button of a comment.
- * 
- * @param object source : element which emitted the event.
- * @param string commentid = identifier of the comment we want to reply to.
- */
-function open_reply(source, commentid) {
-    $('div.reply').remove(); // Remove any other reply area.
-    source.after('<div class="reply">'
-                + '<input type="text" id="nickname" class="form-control" title="Optional nickname..." value="Optional nickname..." />'
-                + '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>'
-                + '<br /><button id="replybutton" onclick="send_comment(\'' + commentid + '\');return false;" class="btn btn-default">Post comment</button>'
-                + '<div id="replystatus"> </div>'
-                + '</div>');
-    $('#nickname').focus(function() {
-        if ($(this).val() == $(this).attr('title')) {
-            $(this).val('');
-        }
-    });
-    $('#replymessage').focus();
-}
+        /**
+         * ask the user for the password and return it
+         *
+         * @throws error when dialog canceled
+         * @return string password
+         */
+        requestPassword: function()
+        {
+            var password = prompt('Please enter the password for this paste:', '');
+            if (password == null) throw 'password prompt canceled';
+            if (password.length == 0) return this.requestPassword();
+            return password;
+        },
+
+        /**
+         * Show decrypted text in the display area, including discussion (if open)
+         *
+         * @param string key : decryption key
+         * @param array comments : Array of messages to display (items = array with keys ('data','meta')
+         */
+        displayMessages: function(key, comments)
+        {
+            // Try to decrypt the paste.
+            var password = this.passwordInput.val();
+            if (!this.prettyPrint.hasClass('prettyprinted')) {
+                try
+                {
+                    var cleartext = filter.decipher(key, password, comments[0].data);
+                    if (cleartext.length == 0)
+                    {
+                        if (password.length == 0) password = this.requestPassword();
+                        cleartext = filter.decipher(key, password, comments[0].data);
+                    }
+                    if (cleartext.length == 0) throw 'failed to decipher message';
+                    this.passwordInput.val(password);
+
+                    helper.setElementText(this.clearText, cleartext);
+                    helper.setElementText(this.prettyPrint, cleartext);
+
+                    // Convert URLs to clickable links.
+                    helper.urls2links(this.clearText);
+                    helper.urls2links(this.prettyPrint);
+                    if (typeof prettyPrint == 'function') prettyPrint();
+                }
+                catch(err)
+                {
+                    this.clearText.addClass('hidden');
+                    this.prettyMessage.addClass('hidden');
+                    this.cloneButton.addClass('hidden');
+                    this.showError('Could not decrypt data (Wrong key?)');
+                    return;
+                }
+            }
 
-/**
- * Send a reply in a discussion.
- * 
- * @param string parentid : the comment identifier we want to send a reply to.
- */
-function send_comment(parentid) {
-    // Do not send if no data.
-    if ($('#replymessage').val().length==0) {
-        return;
-    }
+            // Display paste expiration.
+            if (comments[0].meta.expire_date)
+            {
+                this.remainingTime.removeClass('foryoureyesonly')
+                                   .text('This document will expire in ' + helper.secondsToHuman(comments[0].meta.remaining_time) + '.')
+                                   .removeClass('hidden');
+            }
+            if (comments[0].meta.burnafterreading)
+            {
+                var parent = this;
+                $.get(this.scriptLocation() + '?pasteid=' + this.pasteID() + '&deletetoken=burnafterreading', 'json')
+                .fail(function() {
+                    parent.showError('Could not delete the paste, it was not stored in burn after reading mode.');
+                });
+                this.remainingTime.addClass('foryoureyesonly')
+                                   .text('FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.')
+                                   .removeClass('hidden');
+                // Discourage cloning (as it can't really be prevented).
+                this.cloneButton.addClass('hidden');
+            }
 
-    showStatus('Sending comment...', spin=true);
-    var cipherdata = zeroCipher(pageKey(), $('#replymessage').val());
-    var ciphernickname = '';
-    var nick = $('#nickname').val();
-    if (nick != '' && nick != 'Optional nickname...') {
-        ciphernickname = zeroCipher(pageKey(), nick);
-    }
-    var data_to_send = { data:cipherdata,
-                         parentid: parentid,
-                         pasteid:  pasteID(),
-                         nickname: ciphernickname
-                       };
-
-    $.post(scriptLocation(), data_to_send, function(data) {
-        if (data.status == 0) {
-            showStatus('Comment posted.');
-            $.get(scriptLocation() + "?" + pasteID() + "&json", function(data) {
-                if (data.status == 0) {
-                    displayMessages(pageKey(), data.messages);
+            // If the discussion is opened on this paste, display it.
+            if (comments[0].meta.opendiscussion)
+            {
+                this.comments.html('');
+
+                // iterate over comments
+                for (var i = 1; i < comments.length; i++)
+                {
+                    var place = this.comments;
+                    var comment=comments[i];
+                    var cleartext='[Could not decrypt comment; Wrong key?]';
+                    try
+                    {
+                        cleartext = filter.decipher(key, password, comment.data);
+                    }
+                    catch(err)
+                    {}
+                    // If parent comment exists, display below (CSS will automatically shift it right.)
+                    var cname = '#comment_' + comment.meta.parentid;
+
+                    // If the element exists in page
+                    if ($(cname).length)
+                    {
+                        place = $(cname);
+                    }
+                    var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
+                                   + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
+                                   + '<button class="btn btn-default btn-sm">Reply</button>'
+                                   + '</div></article>');
+                    divComment.find('button').click({commentid: comment.meta.commentid}, $.proxy(this.openReply, this));
+                    helper.setElementText(divComment.find('div.commentdata'), cleartext);
+                    // Convert URLs to clickable links in comment.
+                    helper.urls2links(divComment.find('div.commentdata'));
+
+                    // Try to get optional nickname:
+                    var nick = filter.decipher(key, password, comment.meta.nickname);
+                    if (nick.length > 0)
+                    {
+                        divComment.find('span.nickname').text(nick);
+                    }
+                    else
+                    {
+                        divComment.find('span.nickname').html('<i>(Anonymous)</i>');
+                    }
+                    divComment.find('span.commentdate')
+                              .text(' ('+(new Date(comment.meta.postdate*1000).toString())+')')
+                              .attr('title','CommentID: ' + comment.meta.commentid);
+
+                    // If an avatar is available, display it.
+                    if (comment.meta.vizhash)
+                    {
+                        divComment.find('span.nickname')
+                                  .before('<img src="' + comment.meta.vizhash + '" class="vizhash" title="Anonymous avatar (Vizhash of the IP address)" /> ');
+                    }
+
+                    place.append(divComment);
+                }
+                var divComment = $('<div class="comment"><button class="btn btn-default btn-sm">Add comment</button></div>');
+                divComment.find('button').click({commentid: this.pasteID()}, $.proxy(this.openReply, this));
+                this.comments.append(divComment);
+                this.discussion.removeClass('hidden');
+            }
+        },
+
+        /**
+         * Open the comment entry when clicking the "Reply" button of a comment.
+         *
+         * @param Event event
+         */
+        openReply: function(event)
+        {
+            event.preventDefault();
+            var source = $(event.target),
+                commentid = event.data.commentid;
+
+            // Remove any other reply area.
+            $('div.reply').remove();
+            var reply = $('<div class="reply">' +
+                          '<input type="text" id="nickname" class="form-control" title="Optional nickname..." placeholder="Optional nickname..." />' +
+                          '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>' +
+                          '<br /><button id="replybutton" class="btn btn-default btn-sm">Post comment</button>' +
+                          '<div id="replystatus"> </div>' +
+                          '</div>');
+            reply.find('button').click({parentid: commentid}, $.proxy(this.sendComment, this));
+            source.after(reply);
+            $('#nickname').focus(function() {
+                if ($(this).val() == $(this).attr('title')) $(this).val('');
+            });
+            $('#replymessage').focus();
+        },
+
+        /**
+         * Send a reply in a discussion.
+         *
+         * @param Event event
+         */
+        sendComment: function(event)
+        {
+            event.preventDefault();
+            this.errorMessage.addClass('hidden');
+            // Do not send if no data.
+            var replyMessage = $('#replymessage');
+            if (replyMessage.val().length == 0) return;
+
+            this.showStatus('Sending comment...', true);
+            var parentid = event.data.parentid;
+            var cipherdata = filter.cipher(this.pageKey(), this.passwordInput.val(), replyMessage.val());
+            var ciphernickname = '';
+            var nick = $('#nickname').val();
+            if (nick != '' && nick != 'Optional nickname...')
+            {
+                ciphernickname = filter.cipher(this.pageKey(), this.passwordInput.val(), nick);
+            }
+            var data_to_send = {
+                data:     cipherdata,
+                parentid: parentid,
+                pasteid:  this.pasteID(),
+                nickname: ciphernickname
+            };
+
+            var parent = this;
+            $.post(this.scriptLocation(), data_to_send, function(data)
+            {
+                if (data.status == 0)
+                {
+                    parent.showStatus('Comment posted.', false);
+                    $.get(parent.scriptLocation() + '?' + parent.pasteID() + '&json', function(data)
+                    {
+                        if (data.status == 0)
+                        {
+                            parent.displayMessages(parent.pageKey(), data.messages);
+                        }
+                        else if (data.status == 1)
+                        {
+                            parent.showError('Could not refresh display: ' + data.message);
+                        }
+                        else
+                        {
+                            parent.showError('Could not refresh display: unknown status');
+                        }
+                    }, 'json')
+                    .fail(function() {
+                        parent.showError('Could not refresh display (server error or not responding).');
+                    });
                 }
-                else if (data.status == 1) {
-                    showError('Could not refresh display: ' + data.message);
+                else if (data.status == 1)
+                {
+                    parent.showError('Could not post comment: ' + data.message);
                 }
                 else
                 {
-                    showError('Could not refresh display: unknown status');
+                    parent.showError('Could not post comment: unknown status');
                 }
             }, 'json')
             .fail(function() {
-                showError('Could not refresh display (server error or not responding).');
+                parent.showError('Comment could not be sent (server error or not responding).');
             });
-        }
-        else if (data.status == 1) {
-            showError('Could not post comment: ' + data.message);
-        }
-        else
+        },
+
+        /**
+         * Send a new paste to server
+         *
+         * @param Event event
+         */
+        sendData: function(event)
         {
-            showError('Could not post comment: unknown status');
-        }
-    }, 'json')
-    .fail(function() {
-        showError('Comment could not be sent (server error or not responding).');
-    });
-}
-
-/**
- * Send a new paste to server
- */
-function send_data() {
-    // Do not send if no data.
-    if ($('#message').val().length == 0) {
-        return;
-    }
-
-    // If sjcl has not collected enough entropy yet, display a message.
-    if (!sjcl.random.isReady())
-    {
-        showStatus('Sending paste (Please move your mouse for more entropy)...', spin=true);
-        sjcl.random.addEventListener('seeded', function(){ send_data(); });
-        return;
-    }
-
-    showStatus('Sending paste...', spin=true);
-
-    var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
-    var cipherdata = zeroCipher(randomkey, $('#message').val());
-    var data_to_send = { data:           cipherdata,
-                         expire:         $('#pasteExpiration').val(),
-                         burnafterreading: $('#burnafterreading').is(':checked') ? 1 : 0,
-                         opendiscussion: $('#opendiscussion').is(':checked') ? 1 : 0
-                       };
-    $.post(scriptLocation(), data_to_send, function(data) {
-        if (data.status == 0) {
-            stateExistingPaste();
-            var url = scriptLocation() + "?" + data.id + '#' + randomkey;
-            var deleteUrl = scriptLocation() + "?pasteid=" + data.id + '&deletetoken=' + data.deletetoken;
-            showStatus('');
-
-            $('#pastelink').html('Your paste is <a id="pasteurl" href="' + url + '">' + url + '</a> <span id="copyhint">(Hit CTRL+C to copy)</span>');
-            $('#deletelink').html('<a href="' + deleteUrl + '">Delete data</a>');
-            $('#pasteresult').removeClass('hidden');
-            selectText('pasteurl'); // We pre-select the link so that the user only has to CTRL+C the link.
-
-            setElementText($('#cleartext'), $('#message').val());
-            setElementText($('#prettyprint'), $('#message').val());
-            // Convert URLs to clickable links.
-            urls2links($('#cleartext'));
-            urls2links($('#prettyprint'));
-            showStatus('');
-            if (typeof prettyPrint == 'function') prettyPrint();
-        }
-        else if (data.status==1) {
-            showError('Could not create paste: ' + data.message);
-        }
-        else {
-            showError('Could not create paste.');
-        }
-    }, 'json')
-    .fail(function() {
-        showError('Data could not be sent (server error or not responding).');
-    });
-}
-
-/**
- * Text range selection.
- * From: http://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
- * 
- * @param string element : Indentifier of the element to select (id="").
- */
-function selectText(element) {
-    var doc = document
-        , text = doc.getElementById(element)
-        , range, selection
-    ;
-    if (doc.body.createTextRange) { // MS
-        range = doc.body.createTextRange();
-        range.moveToElementText(text);
-        range.select();
-    } else if (window.getSelection) { // all others
-        selection = window.getSelection();
-        range = doc.createRange();
-        range.selectNodeContents(text);
-        selection.removeAllRanges();
-        selection.addRange(range);
-    }
-}
-
-/**
- * Put the screen in "New paste" mode.
- */
-function stateNewPaste() {
-    $('#sendbutton').removeClass('hidden');
-    $('#clonebutton').addClass('hidden');
-    $('#rawtextbutton').addClass('hidden');
-    $('#expiration').removeClass('hidden');
-    $('#remainingtime').addClass('hidden');
-    $('#burnafterreadingoption').removeClass('hidden');
-    $('#opendisc').removeClass('hidden');
-    $('#newbutton').removeClass('hidden');
-    $('#pasteresult').addClass('hidden');
-    $('#message').text('');
-    $('#message').removeClass('hidden');
-    $('#cleartext').addClass('hidden');
-    $('#message').focus();
-    $('#discussion').addClass('hidden');
-    $('#prettymessage').addClass('hidden');
-    // Show password field
-    $('#password').removeClass('hidden');
-}
-
-/**
- * Put the screen in "Existing paste" mode.
- */
-function stateExistingPaste() {
-    $('#sendbutton').addClass('hidden');
-
-    // No "clone" for IE<10.
-    if ($('#oldienotice').is(":visible")) {
-        $('#clonebutton').addClass('hidden');
-    }
-    else {
-        $('#clonebutton').removeClass('hidden');
-    }
-    $('#rawtextbutton').removeClass('hidden');
-
-    $('#expiration').addClass('hidden');
-    $('#burnafterreadingoption').addClass('hidden');
-    $('#opendisc').addClass('hidden');
-    $('#newbutton').removeClass('hidden');
-    $('#pasteresult').addClass('hidden');
-    $('#message').addClass('hidden');
-    $('#cleartext').addClass('hidden');
-    $('#prettymessage').removeClass('hidden');
-}
-
-/**
- * Return raw text
- */
-function rawText()
-{
-    var paste = $('#cleartext').html();
-    var newDoc = document.open('text/html', 'replace');
-    newDoc.write('<pre>'+paste+'</pre>');
-    newDoc.close();
-}
+            event.preventDefault();
+
+            // Do not send if no data.
+            if (this.message.val().length == 0) return;
+
+            // If sjcl has not collected enough entropy yet, display a message.
+            if (!sjcl.random.isReady())
+            {
+                this.showStatus('Sending paste (Please move your mouse for more entropy)...', true);
+                sjcl.random.addEventListener('seeded', function() {
+                    this.sendData(event);
+                });
+                return;
+            }
 
-/**
- * Clone the current paste.
- */
-function clonePaste() {
-    stateNewPaste();
-    
-    //Erase the id and the key in url
-    history.replaceState(document.title, document.title, scriptLocation());
-    
-    showStatus('');
-    $('#message').text($('#cleartext').text());
-}
+            this.showStatus('Sending paste...', true);
+
+            var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
+            var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val());
+            var data_to_send = {
+                data:             cipherdata,
+                expire:           $('#pasteExpiration').val(),
+                burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
+                opendiscussion:   this.openDiscussion.is(':checked') ? 1 : 0
+            };
+            var parent = this;
+            $.post(this.scriptLocation(), data_to_send, function(data)
+            {
+                if (data.status == 0) {
+                    parent.stateExistingPaste();
+                    var url = parent.scriptLocation() + '?' + data.id + '#' + randomkey;
+                    var deleteUrl = parent.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
+                    parent.showStatus('', false);
+
+                    $('#pastelink').html('Your paste is <a id="pasteurl" href="' + url + '">' + url + '</a> <span id="copyhint">(Hit CTRL+C to copy)</span>');
+                    $('#deletelink').html('<a href="' + deleteUrl + '">Delete data</a>');
+                    parent.pasteResult.removeClass('hidden');
+                    // We pre-select the link so that the user only has to CTRL+C the link.
+                    helper.selectText('pasteurl');
+
+                    helper.setElementText(parent.clearText, parent.message.val());
+                    helper.setElementText(parent.prettyPrint, parent.message.val());
+                    // Convert URLs to clickable links.
+                    helper.urls2links(parent.clearText);
+                    helper.urls2links(parent.prettyPrint);
+                    parent.showStatus('', false);
+                    if (typeof prettyPrint == 'function') prettyPrint();
+                }
+                else if (data.status==1)
+                {
+                    parent.showError('Could not create paste: ' + data.message);
+                }
+                else
+                {
+                    parent.showError('Could not create paste.');
+                }
+            }, 'json')
+            .fail(function() {
+                parent.showError('Data could not be sent (server error or not responding).');
+            });
+        },
 
-/**
- * Create a new paste.
- */
-function newPaste() {
-    stateNewPaste();
-    showStatus('');
-    $('#message').text('');
-}
+        /**
+         * Put the screen in "New paste" mode.
+         */
+        stateNewPaste: function()
+        {
+            this.message.text('');
+            this.cloneButton.addClass('hidden');
+            this.rawTextButton.addClass('hidden');
+            this.remainingTime.addClass('hidden');
+            this.pasteResult.addClass('hidden');
+            this.clearText.addClass('hidden');
+            this.discussion.addClass('hidden');
+            this.prettyMessage.addClass('hidden');
+            this.sendButton.removeClass('hidden');
+            this.expiration.removeClass('hidden');
+            this.burnAfterReadingOption.removeClass('hidden');
+            this.openDisc.removeClass('hidden');
+            this.newButton.removeClass('hidden');
+            this.password.removeClass('hidden');
+            this.message.removeClass('hidden');
+            this.message.focus();
+        },
+
+        /**
+         * Put the screen in "Existing paste" mode.
+         */
+        stateExistingPaste: function()
+        {
+            this.sendButton.addClass('hidden');
 
-/**
- * Display an error message
- * (We use the same function for paste and reply to comments)
- */
-function showError(message) {
-    if ($('#status').length) {
-        $('#status').addClass('errorMessage').text(message);
-    } else {
-        $('#errormessage').removeClass('hidden').append(message);
-    }
-    $('#replystatus').addClass('errorMessage').text(message);
-}
+            // No "clone" for IE<10.
+            if ($('#oldienotice').is(":visible"))
+            {
+                this.cloneButton.addClass('hidden');
+            }
+            else
+            {
+                this.cloneButton.removeClass('hidden');
+            }
+            this.rawTextButton.removeClass('hidden');
+
+            this.expiration.addClass('hidden');
+            this.burnAfterReadingOption.addClass('hidden');
+            this.openDisc.addClass('hidden');
+            this.newButton.removeClass('hidden');
+            this.pasteResult.addClass('hidden');
+            this.message.addClass('hidden');
+            this.clearText.addClass('hidden');
+            this.prettyMessage.removeClass('hidden');
+        },
+
+        /**
+         * If "burn after reading" is checked, disable discussion.
+         */
+        changeBurnAfterReading: function()
+        {
+            if (this.burnAfterReading.is(':checked') )
+            {
+                this.openDisc.addClass('buttondisabled');
+                this.openDiscussion.attr({checked: false, disabled: true});
+            }
+            else
+            {
+                this.openDisc.removeClass('buttondisabled');
+                this.openDiscussion.removeAttr('disabled');
+            }
+        },
+
+        /**
+         * Reload the page
+         *
+         * @param Event event
+         */
+        reloadPage: function(event)
+        {
+            event.preventDefault();
+            window.location.href = this.scriptLocation();
+        },
+
+        /**
+         * Return raw text
+         *
+         * @param Event event
+         */
+        rawText: function(event)
+        {
+            event.preventDefault();
+            var paste = this.clearText.html();
+            var newDoc = document.open('text/html', 'replace');
+            newDoc.write('<pre>' + paste + '</pre>');
+            newDoc.close();
+        },
+
+        /**
+         * Clone the current paste.
+         *
+         * @param Event event
+         */
+        clonePaste: function(event)
+        {
+            event.preventDefault();
+            this.stateNewPaste();
 
-/**
- * Display status
- * (We use the same function for paste and reply to comments)
- *
- * @param string message : text to display
- * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
- */
-function showStatus(message, spin) {
-    $('#replystatus').removeClass('errorMessage');
-    $('#replystatus').text(message);
-    if (!message) {
-        $('#status').html(' ');
-        return;
-    }
-    if (message == '') {
-        $('#status').html(' ');
-        return;
-    }
-    $('#status').removeClass('errorMessage');
-    $('#status').text(message);
-    if (spin) {
-        var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0px 4px 0px 0px;" />';
-        $('#status').prepend(img);
-        $('#replystatus').prepend(img);
-    }
-}
+            // Erase the id and the key in url
+            history.replaceState(document.title, document.title, this.scriptLocation());
 
-/**
- * Convert URLs to clickable links.
- * URLs to handle:
- * <code>
- *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
- *     http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
- *     http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
- * </code>
- *
- * @param object element : a jQuery DOM element.
- */
-function urls2links(element) {
-    var re = /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig;
-    element.html(element.html().replace(re,'<a href="$1" rel="nofollow">$1</a>'));
-    var re = /((magnet):[\w?=&.\/-;#@~%+-]+)/ig;
-    element.html(element.html().replace(re,'<a href="$1">$1</a>'));
-}
+            this.showStatus('', false);
+            this.message.text(this.clearText.text());
+        },
 
-/**
- * Return the deciphering key stored in anchor part of the URL
- */
-function pageKey() {
-    var key = window.location.hash.substring(1);  // Get key
+        /**
+         * Create a new paste.
+         */
+        newPaste: function()
+        {
+            this.stateNewPaste();
+            this.showStatus('', false);
+            this.message.text('');
+        },
+
+        /**
+         * Display an error message
+         * (We use the same function for paste and reply to comments)
+         *
+         * @param string message : text to display
+         */
+        showError: function(message)
+        {
+            if (this.status.length)
+            {
+                this.status.addClass('errorMessage').text(message);
+            }
+            else
+            {
+                this.errorMessage.removeClass('hidden').append(message);
+            }
+            this.replyStatus.addClass('errorMessage').text(message);
+        },
+
+        /**
+         * Display a status message
+         * (We use the same function for paste and reply to comments)
+         *
+         * @param string message : text to display
+         * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
+         */
+        showStatus: function(message, spin)
+        {
+            this.replyStatus.removeClass('errorMessage').text(message);
+            if (!message)
+            {
+                this.status.html(' ');
+                return;
+            }
+            if (message == '')
+            {
+                this.status.html(' ');
+                return;
+            }
+            this.status.removeClass('errorMessage').text(message);
+            if (spin)
+            {
+                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
+                this.status.prepend(img);
+                this.replyStatus.prepend(img);
+            }
+        },
 
-    // Some stupid web 2.0 services and redirectors add data AFTER the anchor
-    // (such as &utm_source=...).
-    // We will strip any additional data.
+        /**
+         * bind events to DOM elements
+         */
+        bindEvents: function()
+        {
+            this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
+            this.sendButton.click($.proxy(this.sendData, this));
+            this.cloneButton.click($.proxy(this.clonePaste, this));
+            this.rawTextButton.click($.proxy(this.rawText, this));
+            $('.reloadlink').click($.proxy(this.reloadPage, this));
+        },
+
+        /**
+         * main application
+         */
+        init: function()
+        {
+            // hide "no javascript" message
+            $('#noscript').hide();
+
+            // preload jQuery wrapped DOM elements and bind events
+            this.burnAfterReading = $('#burnafterreading');
+            this.burnAfterReadingOption = $('#burnafterreadingoption');
+            this.cipherData = $('#cipherdata');
+            this.clearText = $('#cleartext');
+            this.cloneButton = $('#clonebutton');
+            this.comments = $('#comments');
+            this.discussion = $('#discussion');
+            this.errorMessage = $('#errormessage');
+            this.expiration = $('#expiration');
+            this.message = $('#message');
+            this.newButton = $('#newbutton');
+            this.openDisc = $('#opendisc');
+            this.openDiscussion = $('#opendiscussion');
+            this.password = $('#password');
+            this.passwordInput = $('#passwordinput');
+            this.pasteResult = $('#pasteresult');
+            this.prettyMessage = $('#prettymessage');
+            this.prettyPrint = $('#prettyprint');
+            this.rawTextButton = $('#rawtextbutton');
+            this.remainingTime = $('#remainingtime');
+            this.replyStatus = $('#replystatus');
+            this.sendButton = $('#sendbutton');
+            this.status = $('#status');
+            this.bindEvents();
+
+            // Display status returned by php code if any (eg. Paste was properly deleted.)
+            if (this.status.text().length > 0)
+            {
+                this.showStatus(this.status.text(), false);
+                return;
+            }
 
-    // First, strip everything after the equal sign (=) which signals end of base64 string.
-    i = key.indexOf('='); if (i>-1) { key = key.substring(0,i+1); }
+            // Keep line height even if content empty.
+            this.status.html(' ');
 
-    // If the equal sign was not present, some parameters may remain:
-    i = key.indexOf('&'); if (i>-1) { key = key.substring(0,i); }
+            // Display an existing paste
+            if (this.cipherData.text().length > 1)
+            {
+                // Missing decryption key in URL?
+                if (window.location.hash.length == 0)
+                {
+                    this.showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL ?)');
+                    return;
+                }
 
-    // Then add trailing equal sign if it's missing
-    if (key.charAt(key.length-1)!=='=') key+='=';
+                // List of messages to display.
+                var messages = $.parseJSON(this.cipherData.text());
 
-    return key;
-}
+                // Show proper elements on screen.
+                this.stateExistingPaste();
 
-/**
- * main application start, called when DOM is fully loaded
- */
-$(function() {
-    // hide "no javascript" message
-    $('#noscript').hide();
-
-    // If "burn after reading" is checked, disable discussion.
-    $('#burnafterreading').change(function() {
-        if ($(this).is(':checked') ) {
-            $('#opendisc').addClass('buttondisabled');
-            $('#opendiscussion').attr({checked: false});
-            $('#opendiscussion').attr('disabled',true);
-        }
-        else {
-            $('#opendisc').removeClass('buttondisabled');
-            $('#opendiscussion').removeAttr('disabled');
+                this.displayMessages(this.pageKey(), messages);
+            }
+            // Display error message from php code.
+            else if (this.errorMessage.text().length > 1)
+            {
+                this.showError(this.errorMessage.text());
+            }
+            // Create a new paste.
+            else
+            {
+                this.newPaste();
+            }
         }
-    });
-
-    // Display status returned by php code if any (eg. Paste was properly deleted.)
-    if ($('#status').text().length > 0) {
-        showStatus($('#status').text(),false);
-        return;
     }
 
-    $('#status').html(' '); // Keep line height even if content empty.
-
-    // Display an existing paste
-    if ($('#cipherdata').text().length > 1) {
-        // Missing decryption key in URL ?
-        if (window.location.hash.length == 0) {
-            showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL ?)');
-            return;
-        }
-
-        // List of messages to display
-        var messages = jQuery.parseJSON($('#cipherdata').text());
-
-        // Show proper elements on screen.
-        stateExistingPaste();
-
-        displayMessages(pageKey(), messages);
-    }
-    // Display error message from php code.
-    else if ($('#errormessage').text().length>1) {
-        showError($('#errormessage').text());
-    }
-    // Create a new paste.
-    else {
-        newPaste();
-    }
+    /**
+     * main application start, called when DOM is fully loaded
+     */
+    zerobin.init();
 });
+

+ 5 - 5
tpl/bootstrap.html

@@ -33,25 +33,25 @@
 						<span class="icon-bar"></span>
 						<span class="icon-bar"></span>
 					</button>
-				<a class="navbar-brand" href="/" onclick="window.location.href=scriptLocation();return false;">{function="t('ZeroBin')"}</a>
+				<a class="reloadlink navbar-brand" href="/">{function="t('ZeroBin')"}</a>
 				</div>
 				<div id="navbar" class="navbar-collapse collapse">
 					<ul class="nav navbar-nav pull-right">
 						<li>
-							<button id="newbutton" type="button" class="hidden btn btn-default navbar-btn" onclick="window.location.href=scriptLocation();return false;">
+							<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
 								<span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"}
 							</button>
 						</li>
 					</ul>
 					<ul class="nav navbar-nav">
 						<li class="pr">
-							<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn" onclick="send_data();return false;">
+							<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn">
 								<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
 							</button>
-							<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn" onclick="clonePaste();return false;">
+							<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
 								<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
 							</button>
-							<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn" onclick="rawText();return false;">
+							<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
 								<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
 							</button>
 						</li>

+ 6 - 6
tpl/page.html

@@ -24,7 +24,7 @@
 				{function="t('ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://github.com/elrido/ZeroBin/wiki">project page</a>.')"}<br />{if="strlen($NOTICE)"}
 				<span class="blink">▶</span> {$NOTICE}{/if}
 			</div>
-			<h1 title="ZeroBin" onclick="window.location.href=scriptLocation();return false;">{function="t('ZeroBin')"}</h1><br />
+			<h1 class="reloadlink">{function="t('ZeroBin')"}</h1><br />
 			<h2>{function="t('Because ignorance is bliss')"}</h2><br />
 			<h3>{$VERSION}</h3>
 			<div id="noscript" class="nonworking">{function="t('Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.')"}</div>
@@ -41,10 +41,10 @@
 				<div id="status">{$STATUS|htmlspecialchars}</div>
 				<div id="errormessage" class="hidden">{$ERROR|htmlspecialchars}</div>
 				<div id="toolbar">
-					<button id="newbutton" onclick="window.location.href=scriptLocation();return false;" class="hidden"><img src="img/icon_new.png" width="11" height="15" alt="" />{function="t('New')"}</button>
-					<button id="sendbutton" onclick="send_data();return false;" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" />{function="t('Send')"}</button>
-					<button id="clonebutton" onclick="clonePaste();return false;" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" />{function="t('Clone')"}</button>
-					<button id="rawtextbutton" onclick="rawText();return false;" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" />{function="t('Raw text')"}</button>
+					<button id="newbutton" class="reloadlink hidden"><img src="img/icon_new.png" width="11" height="15" alt="" />{function="t('New')"}</button>
+					<button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" />{function="t('Send')"}</button>
+					<button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" />{function="t('Clone')"}</button>
+					<button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" />{function="t('Raw text')"}</button>
 					<div id="expiration" class="hidden">{function="t('Expires')"}:
 						<select id="pasteExpiration" name="pasteExpiration">
 {loop="EXPIRE"}
@@ -61,7 +61,7 @@
 						<label for="opendiscussion" {if="!$OPENDISCUSSION"} style="color: #BBBBBB;"{/if}>{function="t('Open discussion')"}</label>
 					</div>{/if}{if="$PASSWORD"}
 					<div id="password" class="hidden">
-						<input id="passwordinput" placeholder="{function="t('Password (recommended)')"}" size="32" />
+						<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" size="32" />
 					</div>{/if}
 				</div>
 				<div id="pasteresult" class="hidden">