diff --git a/css/bootstrap/zerobin.css b/css/bootstrap/zerobin.css index 3b660a3..a7cd0ff 100644 --- a/css/bootstrap/zerobin.css +++ b/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; +} \ No newline at end of file diff --git a/css/zerobin.css b/css/zerobin.css index d7c964f..0ee0d19 100644 --- a/css/zerobin.css +++ b/css/zerobin.css @@ -65,7 +65,6 @@ h3 { } #aboutbox { - font-size: 1.2em; color: #94a3b4; padding: 4px 8px 4px 16px; position: relative; diff --git a/js/zerobin.js b/js/zerobin.js index fe8576b..5407340 100644 --- a/js/zerobin.js +++ b/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, '&').replace(//g, '>').replace(/"/g, '"'); + }, + + /** + * 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
'); + element.html('
'+html+'
'); + } + // for other (sane) browsers: + else + { + element.text(text); + } + }, + + /** + * Convert URLs to clickable links. + * URLs to handle: + * + * 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= + * + * + * @param object element : a jQuery DOM element. + */ + urls2links: function(element) + { + element.html( + element.html().replace( + /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, + '$1' + ) + ); + element.html( + element.html().replace( + /((magnet):[\w?=&.\/-;#@~%+-]+)/ig, + '$1' + ) + ); + } + }; + + /** + * 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, '&').replace(//g, '>').replace(/"/g, '"'); -} -/** - * 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
"); - element.html('
'+html+'
'); - } - // 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 = $('
' - + '
' - + '' - + '
'); - setElementText(divComment.find('div.commentdata'), cleartext); - // Convert URLs to clickable links in comment. - urls2links(divComment.find('div.commentdata')); - divComment.find('span.nickname').html('(Anonymous)'); - - // 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(' '); - } - - place.append(divComment); - } - $('#comments').append('
'); - $('#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('
' - + '' - + '' - + '
' - + '
' - + '
'); - $('#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 = $('
' + + '
' + + '' + + '
'); + 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('(Anonymous)'); + } + 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(' '); + } + + place.append(divComment); + } + var divComment = $('
'); + 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 = $('
' + + '' + + '' + + '
' + + '
' + + '
'); + 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 ' + url + ' (Hit CTRL+C to copy)'); - $('#deletelink').html('Delete data'); - $('#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('
'+paste+'
'); - 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 ' + url + ' (Hit CTRL+C to copy)'); + $('#deletelink').html('Delete data'); + 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('
' + paste + '
'); + 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 = ''; - $('#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: - * - * 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= - * - * - * @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,'$1')); - var re = /((magnet):[\w?=&.\/-;#@~%+-]+)/ig; - element.html(element.html().replace(re,'$1')); -} + 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 = ''; + 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(); }); + diff --git a/tpl/bootstrap.html b/tpl/bootstrap.html index 28f0518..74c8100 100644 --- a/tpl/bootstrap.html +++ b/tpl/bootstrap.html @@ -33,25 +33,25 @@ - {function="t('ZeroBin')"} + {function="t('ZeroBin')"} -

{function="t('ZeroBin')"}


+

{function="t('ZeroBin')"}


{function="t('Because ignorance is bliss')"}


{$VERSION}

{function="t('Javascript is required for ZeroBin to work.
Sorry for the inconvenience.')"}
@@ -41,10 +41,10 @@
{$STATUS|htmlspecialchars}
- - - - + + + + {/if}