From 2ebcf60516d2991d0869d10dcaad5b2924d804e2 Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 8 Feb 2017 13:20:51 +0100 Subject: [PATCH 01/59] Use revealing module pattern ala http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html Also made the loadTranslations a bit more robust with more error messaged being logged. --- .eslintrc | 2 +- js/privatebin.js | 1118 +++++++++++++++++++++++++-------------------- tpl/bootstrap.php | 2 +- 3 files changed, 617 insertions(+), 505 deletions(-) diff --git a/.eslintrc b/.eslintrc index 97437c7..a5e0c90 100644 --- a/.eslintrc +++ b/.eslintrc @@ -99,7 +99,7 @@ rules: no-with: 2 radix: 2 vars-on-top: 0 - wrap-iife: 2 + wrap-iife: 0 yoda: 0 # Strict diff --git a/js/privatebin.js b/js/privatebin.js index 4fd0e99..ed3baa3 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -25,14 +25,49 @@ // Immediately start random number generator collector. sjcl.random.startCollectors(); +// jQuery(document).ready(function() { +// // startup +// } + jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * static helper methods * + * @param {object} window + * @param {object} document * @name helper * @class */ - var helper = { + var helper = (function (window, document) { + var me = {}; + + /** + * character to HTML entity lookup table + * + * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} + * @private + * @enum {Object} + * @readonly + */ + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; + + /** + * cache for script location + * + * @private + * @enum {string|null} + */ + var scriptLocation = null; + /** * converts a duration (in seconds) into human friendly approximation * @@ -41,7 +76,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {number} seconds * @return {Array} */ - secondsToHuman: function(seconds) + me.secondsToHuman = function(seconds) { var v; if (seconds < 60) @@ -67,7 +102,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } v = Math.floor(seconds / (60 * 60 * 24 * 30)); return [v, 'month']; - }, + }; /** * text range selection @@ -75,47 +110,45 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @see {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse} * @name helper.selectText * @function - * @param {string} element - Indentifier of the element to select (id="") + * @param {HTMLElement} element */ - selectText: function(element) + me.selectText = function(element) { - var doc = document, - text = doc.getElementById(element), - range, - selection; + var range, selection; // MS - if (doc.body.createTextRange) + if (document.body.createTextRange) { - range = doc.body.createTextRange(); - range.moveToElementText(text); + range = document.body.createTextRange(); + range.moveToElementText(element); range.select(); } // all others else if (window.getSelection) { selection = window.getSelection(); - range = doc.createRange(); - range.selectNodeContents(text); + range = document.createRange(); + range.selectNodeContents(element); selection.removeAllRanges(); selection.addRange(range); } - }, + }; /** * set text of a DOM element (required for IE), - * this is equivalent to element.text(text) * * @name helper.setElementText * @function * @param {Object} element - a DOM element * @param {string} text - the text to enter + * @this is equivalent to element.text(text) + * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look */ - setElementText: function(element, text) + me.setElementText = function(element, text) { // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this... if ($('#oldienotice').is(':visible')) { - var html = this.htmlEntities(text).replace(/\n/ig, '\r\n
'); + var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
'); element.html('
' + html + '
'); } // for other (sane) browsers: @@ -123,7 +156,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { element.text(text); } - }, + }; /** * replace last child of element with message @@ -133,7 +166,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {Object} element - a jQuery wrapped DOM element * @param {string} message - the message to append */ - setMessage: function(element, message) + me.setMessage = function(element, message) { var content = element.contents(); if (content.length > 0) @@ -142,9 +175,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } else { - this.setElementText(element, message); + me.setElementText(element, message); } - }, + }; /** * convert URLs to clickable links. @@ -159,7 +192,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Object} element - a jQuery DOM element */ - urls2links: function(element) + me.urls2links = function(element) { var markup = '$1'; element.html( @@ -174,7 +207,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { markup ) ); - }, + }; /** * minimal sprintf emulation for %s and %d formats @@ -186,7 +219,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {...*} args - one or multiple parameters injected into format string * @return {string} */ - sprintf: function() + me.sprintf = function() { var args = arguments; if (typeof arguments[0] === 'object') @@ -218,7 +251,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return val; }); - }, + }; /** * get value of cookie, if it was set, empty string otherwise @@ -229,7 +262,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} cname * @return {string} */ - getCookie: function(cname) { + me.getCookie = function(cname) { var name = cname + '=', ca = document.cookie.split(';'); for (var i = 0; i < ca.length; ++i) { @@ -244,7 +277,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } return ''; - }, + }; /** * get the current script location (without search or hash part of the URL), @@ -254,19 +287,27 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @return {string} current script location */ - scriptLocation: function() + me.scriptLocation = function() { - var scriptLocation = window.location.href.substring( + // check for cached version + if (scriptLocation !== null) { + return scriptLocation; + } + + scriptLocation = window.location.href.substring( 0, window.location.href.length - window.location.search.length - window.location.hash.length - ), - hashIndex = scriptLocation.indexOf('?'); + ); + + var hashIndex = scriptLocation.indexOf('?'); + if (hashIndex !== -1) { scriptLocation = scriptLocation.substring(0, hashIndex); } + return scriptLocation; - }, + }; /** * get the pastes unique identifier from the URL, @@ -276,10 +317,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @return {string} unique identifier */ - pasteId: function() + me.pasteId = function() { return window.location.search.substring(1); - }, + }; /** * return the deciphering key stored in anchor part of the URL @@ -288,7 +329,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @return {string} key */ - pageKey: function() + me.pageKey = function() { var key = window.location.hash.substring(1), i = key.indexOf('&'); @@ -301,7 +342,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return key; - }, + }; /** * convert all applicable characters to HTML entities @@ -312,48 +353,51 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} str * @return {string} escaped HTML */ - htmlEntities: function(str) { + me.htmlEntities = function(str) { return String(str).replace( /[&<>"'`=\/]/g, function(s) { - return helper.entityMap[s]; + return entityMap[s]; }); - }, + }; - /** - * character to HTML entity lookup table - * - * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} - * @name helper.entityMap - * @enum {Object} - * @readonly - */ - entityMap: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - } - }; + return me; + })(window, document); /** * internationalization methods * + * @param {object} window + * @param {object} document * @name i18n * @class */ - var i18n = { + var i18n = (function (window, document) { + var me = {}; + /** * supported languages, minus the built in 'en' * - * @name i18n.supportedLanguages + * @private * @prop {string[]} * @readonly */ - supportedLanguages: ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh'], + var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh']; + + /** + * built in language + * + * @private + * @prop {string} + */ + var language = 'en'; + + /** + * translation cache + * + * @private + * @enum {Object} + */ + var translations = {}; /** * translate a string, alias for i18n.translate() @@ -364,10 +408,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {...*} args - one or multiple parameters injected into placeholders * @return {string} */ - _: function() + me._ = function() { - return this.translate(arguments); - }, + return me.translate(arguments); + }; /** * translate a string @@ -378,7 +422,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {...*} args - one or multiple parameters injected into placeholders * @return {string} */ - translate: function() + me.translate = function() { var args = arguments, messageId; if (typeof arguments[0] === 'object') @@ -399,34 +443,34 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { return messageId; } - if (!this.translations.hasOwnProperty(messageId)) + if (!translations.hasOwnProperty(messageId)) { - if (this.language !== 'en') + if (language !== 'en') { - console.debug( - 'Missing ' + this.language + ' translation for: ' + messageId + console.error( + 'Missing ' + language + ' translation for: ' + messageId ); } - this.translations[messageId] = args[0]; + translations[messageId] = args[0]; } - if (usesPlurals && $.isArray(this.translations[messageId])) + if (usesPlurals && $.isArray(translations[messageId])) { var n = parseInt(args[1] || 1, 10), - key = this.getPluralForm(n), - maxKey = this.translations[messageId].length - 1; + key = me.getPluralForm(n), + maxKey = translations[messageId].length - 1; if (key > maxKey) { key = maxKey; } - args[0] = this.translations[messageId][key]; + args[0] = translations[messageId][key]; args[1] = n; } else { - args[0] = this.translations[messageId]; + args[0] = translations[messageId]; } return helper.sprintf(args); - }, + }; /** * per language functions to use to determine the plural form @@ -437,8 +481,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {number} n * @return {number} array key */ - getPluralForm: function(n) { - switch (this.language) + me.getPluralForm = function(n) { + switch (language) { case 'fr': case 'oc': @@ -454,7 +498,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { default: return (n !== 1 ? 1 : 0); } - }, + }; /** * load translations into cache, then trigger controller initialization @@ -462,52 +506,55 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name i18n.loadTranslations * @function */ - loadTranslations: function() + me.loadTranslations = function() { - var language = helper.getCookie('lang'); - if (language.length === 0) + var newLanguage = helper.getCookie('lang'); + + // auto-select language based on browser settings + if (newLanguage.length === 0) { - language = (navigator.language || navigator.userLanguage).substring(0, 2); + newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2); } - // note that 'en' is built in, so no translation is necessary - if (i18n.supportedLanguages.indexOf(language) === -1) + + // if language is already used (e.g, default 'en'), skip update + if (newLanguage === language) { controller.init(); + return; } - else + + // if language is not supported, show error + if (supportedLanguages.indexOf(newLanguage) === -1) { - $.getJSON('i18n/' + language + '.json', function(data) { - i18n.language = language; - i18n.translations = data; - controller.init(); - }); + console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage); + controller.init(); } - }, - /** - * built in language - * - * @name i18n.language - * @prop {string} - */ - language: 'en', + // load strongs from JSON + $.getJSON('i18n/' + newLanguage + '.json', function(data) { + language = newLanguage; + translations = data; + }).fail(function (data, textStatus, errorMsg) { + console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); + }); - /** - * translation cache - * - * @name i18n.translations - * @enum {Object} - */ - translations: {} - }; + controller.init(); + }; + + return me; + })(window, document); /** * filter methods * + * @param {object} window + * @param {object} document * @name filter * @class */ - var filter = { + var filter = (function (window, document) { + var me = {}; + /** * compress a message (deflate compression), returns base64 encoded data * @@ -516,7 +563,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} message * @return {string} base64 data */ - compress: function(message) + me.compress = function(message) { return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) ); }, @@ -529,7 +576,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} data - base64 data * @return {string} message */ - decompress: function(data) + me.decompress = function(data) { return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) ); }, @@ -544,15 +591,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} message * @return {string} data - JSON with encrypted data */ - cipher: function(key, password, message) + me.cipher = function(key, password, message) { // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit var options = {mode: 'gcm', ks: 256, ts: 128}; if ((password || '').trim().length === 0) { - return sjcl.encrypt(key, this.compress(message), options); + return sjcl.encrypt(key, me.compress(message), options); } - return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.compress(message), options); + return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options); }, /** @@ -565,58 +612,107 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} data - JSON with encrypted data * @return {string} decrypted message */ - decipher: function(key, password, data) + me.decipher = function(key, password, data) { if (data !== undefined) { try { - return this.decompress(sjcl.decrypt(key, data)); + return me.decompress(sjcl.decrypt(key, data)); } catch(err) { try { - return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); + return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); } catch(e) - {} + { + // ignore error, because ????? @TODO + } } } return ''; } - }; + + return me; + })(window, document); /** * PrivateBin logic * + * @param {object} window + * @param {object} document * @name controller * @class */ - var controller = { + var controller = (function (window, document) { + var me = {}; + /** * headers to send in AJAX requests * - * @name controller.headers + * @private * @enum {Object} */ - headers: {'X-Requested-With': 'JSONHttpRequest'}, + var headers = {'X-Requested-With': 'JSONHttpRequest'}; /** * URL shortners create address * - * @name controller.shortenerUrl + * @private * @prop {string} */ - shortenerUrl: '', + var shortenerUrl = ''; /** * URL of newly created paste * - * @name controller.createdPasteUrl + * @private * @prop {string} */ - createdPasteUrl: '', + var createdPasteUrl = ''; + + // jQuery pre-loaded objects + var $attach, + $attachment, + $attachmentLink, + $burnAfterReading, + $burnAfterReadingOption, + $cipherData, + $clearText, + $cloneButton, + $clonedFile, + $comments, + $discussion, + $errorMessage, + $expiration, + $fileRemoveButton, + $fileWrap, + $formatter, + $image, + $loadingIndicator, + $message, + $messageEdit, + $messagePreview, + $newButton, + $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear + $openDiscussion, + $password, + $passwordInput, + $passwordModal, + $passwordForm, + $passwordDecrypt, + $pasteResult, + $pasteUrl, + $prettyMessage, + $prettyPrint, + $preview, + $rawTextButton, + $remainingTime, + $replyStatus, + $sendButton, + $status; /** * ask the user for the password and set it @@ -624,9 +720,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.requestPassword * @function */ - requestPassword: function() + me.requestPassword = function() { - if (this.passwordModal.length === 0) { + if ($passwordModal.length === 0) { var password = prompt(i18n._('Please enter the password for this paste:'), ''); if (password === null) { @@ -634,15 +730,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } if (password.length === 0) { - this.requestPassword(); + // recursive… + me.requestPassword(); } else { - this.passwordInput.val(password); - this.displayMessages(); + $passwordInput.val(password); + me.displayMessages(); } } else { - this.passwordModal.modal(); + $passwordModal.modal(); } - }, + }; /** * use given format on paste, defaults to plain text @@ -652,13 +749,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} format * @param {string} text */ - formatPaste: function(format, text) + me.formatPaste = function(format, text) { - helper.setElementText(this.clearText, text); - helper.setElementText(this.prettyPrint, text); - switch (format || 'plaintext') - { + helper.setElementText($clearText, text); + helper.setElementText($prettyPrint, text); + + switch (format || 'plaintext') { case 'markdown': + // silently fail if showdown is not available + // @TODO: maybe better show an error message? At least a warning? if (typeof showdown === 'object') { var converter = new showdown.Converter({ @@ -666,44 +765,50 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { tables: true, tablesHeaderId: true }); - this.clearText.html( + $clearText.html( converter.makeHtml(text) ); // add table classes from bootstrap css - this.clearText.find('table').addClass('table-condensed table-bordered'); + $clearText.find('table').addClass('table-condensed table-bordered'); - this.clearText.removeClass('hidden'); + $clearText.removeClass('hidden'); + } else { + console.error('showdown is not loaded, could not parse Markdown'); } - this.prettyMessage.addClass('hidden'); + $prettyMessage.addClass('hidden'); break; case 'syntaxhighlighting': + // silently fail if prettyprint is not available + // @TODO: maybe better show an error message? At least a warning? if (typeof prettyPrintOne === 'function') { if (typeof prettyPrint === 'function') { prettyPrint(); } - this.prettyPrint.html( + $prettyPrint.html( prettyPrintOne( helper.htmlEntities(text), null, true ) ); + } else { + console.error('pretty print is not loaded, could not link '); } // fall through, as the rest is the same - default: + default: // = 'plaintext' // convert URLs to clickable links - helper.urls2links(this.clearText); - helper.urls2links(this.prettyPrint); - this.clearText.addClass('hidden'); - if (format === 'plaintext') - { - this.prettyPrint.css('white-space', 'pre-wrap'); - this.prettyPrint.css('word-break', 'normal'); - this.prettyPrint.removeClass('prettyprint'); - } - this.prettyMessage.removeClass('hidden'); + helper.urls2links($clearText); + helper.urls2links($prettyPrint); + $clearText.addClass('hidden'); + + + $prettyPrint.css('white-space', 'pre-wrap'); + $prettyPrint.css('word-break', 'normal'); + $prettyPrint.removeClass('prettyprint'); + + $prettyMessage.removeClass('hidden'); } - }, + }; /** * show decrypted text in the display area, including discussion (if open) @@ -712,12 +817,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) */ - displayMessages: function(paste) + me.displayMessages = function(paste) { - paste = paste || $.parseJSON(this.cipherData.text()); + paste = paste || $.parseJSON($cipherData.text()); var key = helper.pageKey(), - password = this.passwordInput.val(); - if (!this.prettyPrint.hasClass('prettyprinted')) { + password = $passwordInput.val(); + if (!$prettyPrint.hasClass('prettyprinted')) { // Try to decrypt the paste. try { @@ -728,7 +833,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { if (password.length === 0) { - this.requestPassword(); + me.requestPassword(); return; } attachment = filter.decipher(key, password, paste.attachment); @@ -743,28 +848,28 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var attachmentname = filter.decipher(key, password, paste.attachmentname); if (attachmentname.length > 0) { - this.attachmentLink.attr('download', attachmentname); + $attachmentLink.attr('download', attachmentname); } } - this.attachmentLink.attr('href', attachment); - this.attachment.removeClass('hidden'); + $attachmentLink.attr('href', attachment); + $attachment.removeClass('hidden'); // if the attachment is an image, display it var imagePrefix = 'data:image/'; if (attachment.substring(0, imagePrefix.length) === imagePrefix) { - this.image.html( + $image.html( $(document.createElement('img')) .attr('src', attachment) .attr('class', 'img-thumbnail') ); - this.image.removeClass('hidden'); + $image.removeClass('hidden'); } } var cleartext = filter.decipher(key, password, paste.data); if (cleartext.length === 0 && password.length === 0 && !paste.attachment) { - this.requestPassword(); + me.requestPassword(); return; } if (cleartext.length === 0 && !paste.attachment) @@ -772,17 +877,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { throw 'failed to decipher message'; } - this.passwordInput.val(password); + $passwordInput.val(password); if (cleartext.length > 0) { $('#pasteFormatter').val(paste.meta.formatter); - this.formatPaste(paste.meta.formatter, cleartext); + me.formatPaste(paste.meta.formatter, cleartext); } } catch(err) { - this.stateOnlyNewPaste(); - this.showError(i18n._('Could not decrypt data (Wrong key?)')); + me.stateOnlyNewPaste(); + me.showError(i18n._('Could not decrypt data (Wrong key?)')); return; } } @@ -795,8 +900,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { 'This document will expire in %d ' + expiration[1] + '.', 'This document will expire in %d ' + expiration[1] + 's.' ]; - helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0])); - this.remainingTime.removeClass('foryoureyesonly') + helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0])); + $remainingTime.removeClass('foryoureyesonly') .removeClass('hidden'); } if (paste.meta.burnafterreading) @@ -807,29 +912,29 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { url: helper.scriptLocation() + '?' + helper.pasteId(), data: {deletetoken: 'burnafterreading'}, dataType: 'json', - headers: this.headers + headers: headers }) .fail(function() { controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }); - helper.setMessage(this.remainingTime, i18n._( + helper.setMessage($remainingTime, i18n._( 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' )); - this.remainingTime.addClass('foryoureyesonly') + $remainingTime.addClass('foryoureyesonly') .removeClass('hidden'); // discourage cloning (as it can't really be prevented) - this.cloneButton.addClass('hidden'); + $cloneButton.addClass('hidden'); } // if the discussion is opened on this paste, display it if (paste.meta.opendiscussion) { - this.comments.html(''); + $comments.html(''); // iterate over comments for (var i = 0; i < paste.comments.length; ++i) { - var place = this.comments, + var $place = $comments, comment = paste.comments[i], commenttext = filter.decipher(key, password, comment.data), // if parent comment exists, display below (CSS will automatically shift it to the right) @@ -845,9 +950,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // if the element exists in page if ($(cname).length) { - place = $(cname); + $place = $(cname); } - divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this)); + divComment.find('button').click({commentid: comment.id}, $.proxy(me.openReply, me)); helper.setElementText(divCommentData, commenttext); helper.urls2links(divCommentData); @@ -875,17 +980,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ); } - place.append(divComment); + $place.append(divComment); } var divComment = $( '
' ); - divComment.find('button').click({commentid: helper.pasteId()}, $.proxy(this.openReply, this)); - this.comments.append(divComment); - this.discussion.removeClass('hidden'); + divComment.find('button').click({commentid: helper.pasteId()}, $.proxy(me.openReply, me)); + $comments.append(divComment); + $discussion.removeClass('hidden'); } - }, + }; /** * open the comment entry when clicking the "Reply" button of a comment @@ -894,7 +999,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - openReply: function(event) + me.openReply = function(event) { event.preventDefault(); @@ -915,12 +1020,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ); reply.find('button').click( {parentid: commentid}, - $.proxy(this.sendComment, this) + $.proxy(me.sendComment, me) ); source.after(reply); - this.replyStatus = $('#replystatus'); + $replyStatus = $('#replystatus'); $('#replymessage').focus(); - }, + }; /** * send a reply in a discussion @@ -929,10 +1034,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - sendComment: function(event) + me.sendComment = function(event) { event.preventDefault(); - this.errorMessage.addClass('hidden'); + $errorMessage.addClass('hidden'); // do not send if no data var replyMessage = $('#replymessage'); if (replyMessage.val().length === 0) @@ -940,15 +1045,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - this.showStatus(i18n._('Sending comment...'), true); + me.showStatus(i18n._('Sending comment...'), true); var parentid = event.data.parentid, key = helper.pageKey(), - cipherdata = filter.cipher(key, this.passwordInput.val(), replyMessage.val()), + cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), ciphernickname = '', nick = $('#nickname').val(); if (nick.length > 0) { - ciphernickname = filter.cipher(key, this.passwordInput.val(), nick); + ciphernickname = filter.cipher(key, $passwordInput.val(), nick); } var data_to_send = { data: cipherdata, @@ -962,7 +1067,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { url: helper.scriptLocation(), data: data_to_send, dataType: 'json', - headers: this.headers, + headers: headers, success: function(data) { if (data.status === 0) @@ -972,7 +1077,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { type: 'GET', url: helper.scriptLocation() + '?' + helper.pasteId(), dataType: 'json', - headers: controller.headers, + headers: headers, success: function(data) { if (data.status === 0) @@ -1006,7 +1111,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { .fail(function() { controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); }); - }, + }; /** * send a new paste to server @@ -1015,14 +1120,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - sendData: function(event) + me.sendData = function(event) { event.preventDefault(); var file = document.getElementById('file'), files = (file && file.files) ? file.files : null; // FileList object // do not send if no data. - if (this.message.val().length === 0 && !(files && files[0])) + if ($message.val().length === 0 && !(files && files[0])) { return; } @@ -1030,28 +1135,28 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // if sjcl has not collected enough entropy yet, display a message if (!sjcl.random.isReady()) { - this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); + me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); sjcl.random.addEventListener('seeded', function() { - this.sendData(event); + me.sendData(event); }); return; } $('.navbar-toggle').click(); - this.password.addClass('hidden'); - this.showStatus(i18n._('Sending paste...'), true); + $password.addClass('hidden'); + me.showStatus(i18n._('Sending paste...'), true); - this.stateSubmittingPaste(); + me.stateSubmittingPaste(); var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), - password = this.passwordInput.val(); + password = $passwordInput.val(); if(files && files[0]) { if(typeof FileReader === undefined) { // revert loading status… - this.stateNewPaste(); - this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + me.stateNewPaste(); + me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); return; } var reader = new FileReader(); @@ -1068,19 +1173,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(files[0]); reader.readAsDataURL(files[0]); } - else if(this.attachmentLink.attr('href')) + else if($attachmentLink.attr('href')) { - this.sendDataContinue( + me.sendDataContinue( randomkey, - filter.cipher(randomkey, password, this.attachmentLink.attr('href')), - this.attachmentLink.attr('download') + filter.cipher(randomkey, password, $attachmentLink.attr('href')), + $attachmentLink.attr('download') ); } else { - this.sendDataContinue(randomkey, '', ''); + me.sendDataContinue(randomkey, '', ''); } - }, + }; /** * send a new paste to server, step 2 @@ -1091,15 +1196,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} cipherdata_attachment * @param {string} cipherdata_attachment_name */ - sendDataContinue: function(randomkey, cipherdata_attachment, cipherdata_attachment_name) + me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) { - var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val()), + var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), data_to_send = { data: cipherdata, expire: $('#pasteExpiration').val(), formatter: $('#pasteFormatter').val(), - burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0, - opendiscussion: this.openDiscussion.is(':checked') ? 1 : 0 + burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, + opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 }; if (cipherdata_attachment.length > 0) { @@ -1114,15 +1219,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { url: helper.scriptLocation(), data: data_to_send, dataType: 'json', - headers: this.headers, + headers: headers, success: function(data) { if (data.status === 0) { - controller.stateExistingPaste(); + me.stateExistingPaste(); var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - controller.showStatus(''); - controller.errorMessage.addClass('hidden'); + me.showStatus(''); + $errorMessage.addClass('hidden'); // show new URL in browser bar history.pushState({type: 'newpaste'}, document.title, url); @@ -1130,23 +1235,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { i18n._( 'Your paste is %s (Hit [Ctrl]+[c] to copy)', url, url - ) + controller.shortenUrl(url) + ) + me.shortenUrl(url) ); // save newly created element - controller.pasteUrl = $('#pasteurl'); + $pasteUrl = $('#pasteurl'); // and add click event - controller.pasteUrl.click($.proxy(controller.pasteLinkClick, controller)); + $pasteUrl.click($.proxy(me.pasteLinkClick, me)); var shortenButton = $('#shortenbutton'); if (shortenButton) { - shortenButton.click($.proxy(controller.sendToShortener, controller)); + shortenButton.click($.proxy(me.sendToShortener, me)); } $('#deletelink').html('' + i18n._('Delete data') + ''); - controller.pasteResult.removeClass('hidden'); + $pasteResult.removeClass('hidden'); // we pre-select the link so that the user only has to [Ctrl]+[c] the link - helper.selectText('pasteurl'); - controller.showStatus(''); - controller.formatPaste(data_to_send.formatter, controller.message.val()); + helper.selectText($pasteUrl[0]); + me.showStatus(''); + me.formatPaste(data_to_send.formatter, $message.val()); } else if (data.status === 1) { @@ -1165,10 +1270,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { .fail(function() { // revert loading status… - this.stateNewPaste(); + me.stateNewPaste(); controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); }); - }, + }; /** * check if a URL shortener was defined and create HTML containing a link to it @@ -1178,16 +1283,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} url * @return {string} html */ - shortenUrl: function(url) + me.shortenUrl = function(url) { var shortenerHtml = $('#shortenbutton'); if (shortenerHtml) { - this.shortenerUrl = shortenerHtml.data('shortener'); - this.createdPasteUrl = url; + shortenerUrl = shortenerHtml.data('shortener'); + createdPasteUrl = url; return ' ' + $('
').append(shortenerHtml.clone()).html(); } return ''; - }, + }; /** * put the screen in "New paste" mode @@ -1195,30 +1300,30 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.stateNewPaste * @function */ - stateNewPaste: function() + me.stateNewPaste = function() { - this.message.text(''); - this.attachment.addClass('hidden'); - this.cloneButton.addClass('hidden'); - this.rawTextButton.addClass('hidden'); - this.remainingTime.addClass('hidden'); - this.pasteResult.addClass('hidden'); - this.clearText.addClass('hidden'); - this.discussion.addClass('hidden'); - this.prettyMessage.addClass('hidden'); - this.loadingIndicator.addClass('hidden'); - this.sendButton.removeClass('hidden'); - this.expiration.removeClass('hidden'); - this.formatter.removeClass('hidden'); - this.burnAfterReadingOption.removeClass('hidden'); - this.openDisc.removeClass('hidden'); - this.newButton.removeClass('hidden'); - this.password.removeClass('hidden'); - this.attach.removeClass('hidden'); - this.message.removeClass('hidden'); - this.preview.removeClass('hidden'); - this.message.focus(); - }, + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + $sendButton.removeClass('hidden'); + $expiration.removeClass('hidden'); + $formatter.removeClass('hidden'); + $burnAfterReadingOption.removeClass('hidden'); + $openDisc.removeClass('hidden'); + $newButton.removeClass('hidden'); + $password.removeClass('hidden'); + $attach.removeClass('hidden'); + $message.removeClass('hidden'); + $preview.removeClass('hidden'); + $message.focus(); + }; /** * put the screen in mode after submitting a paste @@ -1226,30 +1331,30 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.stateSubmittingPaste * @function */ - stateSubmittingPaste: function() + me.stateSubmittingPaste = function() { - this.message.text(''); - this.attachment.addClass('hidden'); - this.cloneButton.addClass('hidden'); - this.rawTextButton.addClass('hidden'); - this.remainingTime.addClass('hidden'); - this.pasteResult.addClass('hidden'); - this.clearText.addClass('hidden'); - this.discussion.addClass('hidden'); - this.prettyMessage.addClass('hidden'); - this.sendButton.addClass('hidden'); - this.expiration.addClass('hidden'); - this.formatter.addClass('hidden'); - this.burnAfterReadingOption.addClass('hidden'); - this.openDisc.addClass('hidden'); - this.newButton.addClass('hidden'); - this.password.addClass('hidden'); - this.attach.addClass('hidden'); - this.message.addClass('hidden'); - this.preview.addClass('hidden'); - - this.loadingIndicator.removeClass('hidden'); - }, + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $sendButton.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $newButton.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); + $preview.addClass('hidden'); + + $loadingIndicator.removeClass('hidden'); + }; /** * put the screen in a state where the only option is to submit a @@ -1258,30 +1363,30 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.stateOnlyNewPaste * @function */ - stateOnlyNewPaste: function() + me.stateOnlyNewPaste = function() { - this.message.text(''); - this.attachment.addClass('hidden'); - this.cloneButton.addClass('hidden'); - this.rawTextButton.addClass('hidden'); - this.remainingTime.addClass('hidden'); - this.pasteResult.addClass('hidden'); - this.clearText.addClass('hidden'); - this.discussion.addClass('hidden'); - this.prettyMessage.addClass('hidden'); - this.sendButton.addClass('hidden'); - this.expiration.addClass('hidden'); - this.formatter.addClass('hidden'); - this.burnAfterReadingOption.addClass('hidden'); - this.openDisc.addClass('hidden'); - this.password.addClass('hidden'); - this.attach.addClass('hidden'); - this.message.addClass('hidden'); - this.preview.addClass('hidden'); - this.loadingIndicator.addClass('hidden'); - - this.newButton.removeClass('hidden'); - }, + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $sendButton.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); + $preview.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + + $newButton.removeClass('hidden'); + }; /** * put the screen in "Existing paste" mode @@ -1290,7 +1395,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false */ - stateExistingPaste: function(preview) + me.stateExistingPaste = function(preview) { preview = preview || false; @@ -1299,30 +1404,30 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // no "clone" for IE<10. if ($('#oldienotice').is(":visible")) { - this.cloneButton.addClass('hidden'); + $cloneButton.addClass('hidden'); } else { - this.cloneButton.removeClass('hidden'); + $cloneButton.removeClass('hidden'); } - this.rawTextButton.removeClass('hidden'); - this.sendButton.addClass('hidden'); - this.attach.addClass('hidden'); - this.expiration.addClass('hidden'); - this.formatter.addClass('hidden'); - this.burnAfterReadingOption.addClass('hidden'); - this.openDisc.addClass('hidden'); - this.newButton.removeClass('hidden'); - this.preview.addClass('hidden'); - } - - this.pasteResult.addClass('hidden'); - this.message.addClass('hidden'); - this.clearText.addClass('hidden'); - this.prettyMessage.addClass('hidden'); - this.loadingIndicator.addClass('hidden'); - }, + $rawTextButton.removeClass('hidden'); + $sendButton.addClass('hidden'); + $attach.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $newButton.removeClass('hidden'); + $preview.addClass('hidden'); + } + + $pasteResult.addClass('hidden'); + $message.addClass('hidden'); + $clearText.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + }; /** * when "burn after reading" is checked, disable discussion @@ -1330,19 +1435,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.changeBurnAfterReading * @function */ - changeBurnAfterReading: function() + me.changeBurnAfterReading = function() { - if (this.burnAfterReading.is(':checked') ) + if ($burnAfterReading.is(':checked') ) { - this.openDisc.addClass('buttondisabled'); - this.openDiscussion.attr({checked: false, disabled: true}); + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); } else { - this.openDisc.removeClass('buttondisabled'); - this.openDiscussion.removeAttr('disabled'); + $openDisc.removeClass('buttondisabled'); + $openDiscussion.removeAttr('disabled'); } - }, + }; /** * when discussion is checked, disable "burn after reading" @@ -1350,19 +1455,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.changeOpenDisc * @function */ - changeOpenDisc: function() + me.changeOpenDisc = function() { - if (this.openDiscussion.is(':checked') ) + if ($openDiscussion.is(':checked') ) { - this.burnAfterReadingOption.addClass('buttondisabled'); - this.burnAfterReading.attr({checked: false, disabled: true}); + $burnAfterReadingOption.addClass('buttondisabled'); + $burnAfterReading.attr({checked: false, disabled: true}); } else { - this.burnAfterReadingOption.removeClass('buttondisabled'); - this.burnAfterReading.removeAttr('disabled'); + $burnAfterReadingOption.removeClass('buttondisabled'); + $burnAfterReading.removeAttr('disabled'); } - }, + }; /** * forward to URL shortener @@ -1371,11 +1476,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - sendToShortener: function(event) + me.sendToShortener = function(event) { + window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl); event.preventDefault(); - window.location.href = this.shortenerUrl + encodeURIComponent(this.createdPasteUrl); - }, + }; /** * reload the page @@ -1386,11 +1491,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - reloadPage: function(event) + me.reloadPage = function(event) { - event.preventDefault(); window.location.href = helper.scriptLocation(); - }, + event.preventDefault(); + }; /** * return raw text @@ -1399,11 +1504,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - rawText: function(event) + me.rawText = function(event) { - event.preventDefault(); var paste = $('#pasteFormatter').val() === 'markdown' ? - this.prettyPrint.text() : this.clearText.text(); + $prettyPrint.text() : $clearText.text(); history.pushState( null, document.title, helper.scriptLocation() + '?' + helper.pasteId() + '#' + helper.pageKey() @@ -1413,7 +1517,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var newDoc = document.open('text/html', 'replace'); newDoc.write('
' + helper.htmlEntities(paste) + '
'); newDoc.close(); - }, + + event.preventDefault(); + }; /** * clone the current paste @@ -1422,26 +1528,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - clonePaste: function(event) + me.clonePaste = function(event) { event.preventDefault(); - this.stateNewPaste(); + me.stateNewPaste(); // erase the id and the key in url history.replaceState(null, document.title, helper.scriptLocation()); - this.showStatus(''); - if (this.attachmentLink.attr('href')) + me.showStatus(''); + if ($attachmentLink.attr('href')) { - this.clonedFile.removeClass('hidden'); - this.fileWrap.addClass('hidden'); + $clonedFile.removeClass('hidden'); + $fileWrap.addClass('hidden'); } - this.message.text( + $message.text( $('#pasteFormatter').val() === 'markdown' ? - this.prettyPrint.text() : this.clearText.text() + $prettyPrint.text() : $clearText.text() ); $('.navbar-toggle').click(); - }, + }; /** * set the expiration on bootstrap templates @@ -1450,13 +1556,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - setExpiration: function(event) + me.setExpiration = function(event) { event.preventDefault(); var target = $(event.target); $('#pasteExpiration').val(target.data('expiration')); $('#pasteExpirationDisplay').text(target.text()); - }, + }; /** * set the format on bootstrap templates @@ -1465,17 +1571,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - setFormat: function(event) + me.setFormat = function(event) { - event.preventDefault(); var target = $(event.target); $('#pasteFormatter').val(target.data('format')); $('#pasteFormatterDisplay').text(target.text()); - if (this.messagePreview.parent().hasClass('active')) { - this.viewPreview(event); + if ($messagePreview.parent().hasClass('active')) { + me.viewPreview(event); } - }, + event.preventDefault(); + }; /** * set the language in a cookie and reload the page @@ -1484,11 +1590,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - setLanguage: function(event) + me.setLanguage = function(event) { document.cookie = 'lang=' + $(event.target).data('lang'); - this.reloadPage(event); - }, + me.reloadPage(event); + }; /** * support input of tab character @@ -1496,8 +1602,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.supportTabs * @function * @param {Event} event + * @TODO doc what is @this here? */ - supportTabs: function(event) + me.supportTabs = function(event) { var keyCode = event.keyCode || event.which; // tab was pressed @@ -1514,7 +1621,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // put caret at right position again this.selectionStart = this.selectionEnd = start + 1; } - }, + }; /** * view the editor tab @@ -1523,14 +1630,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - viewEditor: function(event) + me.viewEditor = function(event) { + $messagePreview.parent().removeClass('active'); + $messageEdit.parent().addClass('active'); + $message.focus(); + me.stateNewPaste(); + event.preventDefault(); - this.messagePreview.parent().removeClass('active'); - this.messageEdit.parent().addClass('active'); - this.message.focus(); - this.stateNewPaste(); - }, + }; /** * view the preview tab @@ -1539,15 +1647,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - viewPreview: function(event) + me.viewPreview = function(event) { + $messageEdit.parent().removeClass('active'); + $messagePreview.parent().addClass('active'); + $message.focus(); + me.stateExistingPaste(true); + me.formatPaste($('#pasteFormatter').val(), $message.val()); + event.preventDefault(); - this.messageEdit.parent().removeClass('active'); - this.messagePreview.parent().addClass('active'); - this.message.focus(); - this.stateExistingPaste(true); - this.formatPaste($('#pasteFormatter').val(), this.message.val()); - }, + }; /** * handle history (pop) state changes @@ -1558,7 +1667,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - historyChange: function(event) + me.historyChange = function(event) { var currentLocation = helper.scriptLocation(); if (event.originalEvent.state === null && // no state object passed @@ -1568,7 +1677,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // redirect to home page window.location.href = currentLocation; } - }, + }; /** * Forces opening the paste if the link does not do this automatically. @@ -1580,14 +1689,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - pasteLinkClick: function(event) + me.pasteLinkClick = function(event) { // check if location is (already) shown in URL bar - if (window.location.href === this.pasteUrl.attr('href')) { + if (window.location.href === $pasteUrl.attr('href')) { // if so we need to load link by reloading the current site window.location.reload(true); } - }, + }; /** * create a new paste @@ -1595,14 +1704,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.newPaste * @function */ - newPaste: function() + me.newPaste = function() { - this.stateNewPaste(); - this.showStatus(''); - this.message.text(''); - this.changeBurnAfterReading(); - this.changeOpenDisc(); - }, + me.stateNewPaste(); + me.showStatus(''); + $message.text(''); + me.changeBurnAfterReading(); + me.changeOpenDisc(); + }; /** * removes an attachment @@ -1610,15 +1719,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.removeAttachment * @function */ - removeAttachment: function() + me.removeAttachment = function() { - this.clonedFile.addClass('hidden'); + $clonedFile.addClass('hidden'); // removes the saved decrypted file data - this.attachmentLink.attr('href', ''); - // the only way to deselect the file is to recreate the input - this.fileWrap.html(this.fileWrap.html()); - this.fileWrap.removeClass('hidden'); - }, + $attachmentLink.attr('href', ''); + // the only way to deselect the file is to recreate the input // @TODO really? + $fileWrap.html($fileWrap.html()); + $fileWrap.removeClass('hidden'); + }; /** * decrypt using the password from the modal dialog @@ -1626,11 +1735,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.decryptPasswordModal * @function */ - decryptPasswordModal: function() + me.decryptPasswordModal = function() { - this.passwordInput.val(this.passwordDecrypt.val()); - this.displayMessages(); - }, + $passwordInput.val($passwordDecrypt.val()); + me.displayMessages(); + }; /** * submit a password in the modal dialog @@ -1639,11 +1748,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {Event} event */ - submitPasswordModal: function(event) + me.submitPasswordModal = function(event) { event.preventDefault(); - this.passwordModal.modal('hide'); - }, + $passwordModal.modal('hide'); + }; /** * display an error message, @@ -1653,30 +1762,30 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {string} message - text to display */ - showError: function(message) + me.showError = function(message) { - if (this.status.length) + if ($status.length) { - this.status.addClass('errorMessage').text(message); + $status.addClass('errorMessage').text(message); } else { - this.errorMessage.removeClass('hidden'); - helper.setMessage(this.errorMessage, message); + $errorMessage.removeClass('hidden'); + helper.setMessage($errorMessage, message); } - if (typeof this.replyStatus !== 'undefined') { - this.replyStatus.addClass('errorMessage'); - this.replyStatus.addClass(this.errorMessage.attr('class')); - if (this.status.length) + if (typeof $replyStatus !== 'undefined') { + $replyStatus.addClass('errorMessage'); + $replyStatus.addClass($errorMessage.attr('class')); + if ($status.length) { - this.replyStatus.html(this.status.html()); + $replyStatus.html($status.html()); } else { - this.replyStatus.html(this.errorMessage.html()); + $replyStatus.html($errorMessage.html()); } } - }, + }; /** * display a status message, @@ -1687,66 +1796,66 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @param {string} message - text to display * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false */ - showStatus: function(message, spin) + me.showStatus = function(message, spin) { if (spin || false) { var img = ''; - this.status.prepend(img); - if (typeof this.replyStatus !== 'undefined') { - this.replyStatus.prepend(img); + $status.prepend(img); + if (typeof $replyStatus !== 'undefined') { + $replyStatus.prepend(img); } } - if (typeof this.replyStatus !== 'undefined') { - this.replyStatus.removeClass('errorMessage').text(message); + if (typeof $replyStatus !== 'undefined') { + $replyStatus.removeClass('errorMessage').text(message); } if (!message) { - this.status.html(' '); + $status.html(' '); return; } if (message === '') { - this.status.html(' '); + $status.html(' '); return; } - this.status.removeClass('errorMessage').text(message); - }, + $status.removeClass('errorMessage').text(message); + }; /** * bind events to DOM elements * - * @name controller.bindEvents + * @private * @function */ - bindEvents: function() + function bindEvents() { - this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this)); - this.openDisc.change($.proxy(this.changeOpenDisc, this)); - this.sendButton.click($.proxy(this.sendData, this)); - this.cloneButton.click($.proxy(this.clonePaste, this)); - this.rawTextButton.click($.proxy(this.rawText, this)); - this.fileRemoveButton.click($.proxy(this.removeAttachment, this)); - $('.reloadlink').click($.proxy(this.reloadPage, this)); - this.message.keydown(this.supportTabs); - this.messageEdit.click($.proxy(this.viewEditor, this)); - this.messagePreview.click($.proxy(this.viewPreview, this)); + $burnAfterReading.change($.proxy(me.changeBurnAfterReading, me)); + $openDisc.change($.proxy(me.changeOpenDisc, me)); + $sendButton.click($.proxy(me.sendData, me)); + $cloneButton.click($.proxy(me.clonePaste, me)); + $rawTextButton.click($.proxy(me.rawText, me)); + $fileRemoveButton.click($.proxy(me.removeAttachment, me)); + $('.reloadlink').click($.proxy(me.reloadPage, me)); + $message.keydown(me.supportTabs); + $messageEdit.click($.proxy(me.viewEditor, me)); + $messagePreview.click($.proxy(me.viewPreview, me)); // bootstrap template drop downs - $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(this.setExpiration, this)); - $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(this.setFormat, this)); - $('#language ul.dropdown-menu li a').click($.proxy(this.setLanguage, this)); + $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(me.setExpiration, me)); + $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(me.setFormat, me)); + $('#language ul.dropdown-menu li a').click($.proxy(me.setLanguage, me)); // page template drop down - $('#language select option').click($.proxy(this.setLanguage, this)); + $('#language select option').click($.proxy(me.setLanguage, me)); // handle modal password request on decryption - this.passwordModal.on('shown.bs.modal', $.proxy(this.passwordDecrypt.focus, this)); - this.passwordModal.on('hidden.bs.modal', $.proxy(this.decryptPasswordModal, this)); - this.passwordForm.submit($.proxy(this.submitPasswordModal, this)); + $passwordModal.on('shown.bs.modal', $.proxy($passwordDecrypt.focus, me)); + $passwordModal.on('hidden.bs.modal', $.proxy(me.decryptPasswordModal, me)); + $passwordForm.submit($.proxy(me.submitPasswordModal, me)); - $(window).on('popstate', $.proxy(this.historyChange, this)); - }, + $(window).on('popstate', $.proxy(me.historyChange, me)); + }; /** * main application @@ -1754,89 +1863,92 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @name controller.init * @function */ - init: function() + me.init = function() { // hide "no javascript" message $('#noscript').hide(); // preload jQuery wrapped DOM elements and bind events - this.attach = $('#attach'); - this.attachment = $('#attachment'); - this.attachmentLink = $('#attachment a'); - this.burnAfterReading = $('#burnafterreading'); - this.burnAfterReadingOption = $('#burnafterreadingoption'); - this.cipherData = $('#cipherdata'); - this.clearText = $('#cleartext'); - this.cloneButton = $('#clonebutton'); - this.clonedFile = $('#clonedfile'); - this.comments = $('#comments'); - this.discussion = $('#discussion'); - this.errorMessage = $('#errormessage'); - this.expiration = $('#expiration'); - this.fileRemoveButton = $('#fileremovebutton'); - this.fileWrap = $('#filewrap'); - this.formatter = $('#formatter'); - this.image = $('#image'); - this.loadingIndicator = $('#loadingindicator'); - this.message = $('#message'); - this.messageEdit = $('#messageedit'); - this.messagePreview = $('#messagepreview'); - this.newButton = $('#newbutton'); - this.openDisc = $('#opendisc'); - this.openDiscussion = $('#opendiscussion'); - this.password = $('#password'); - this.passwordInput = $('#passwordinput'); - this.passwordModal = $('#passwordmodal'); - this.passwordForm = $('#passwordform'); - this.passwordDecrypt = $('#passworddecrypt'); - this.pasteResult = $('#pasteresult'); - // this.pasteUrl is saved in sendDataContinue() if/after it is + $attach = $('#attach'); + $attachment = $('#attachment'); + $attachmentLink = $('#attachment a'); + $burnAfterReading = $('#burnafterreading'); + $burnAfterReadingOption = $('#burnafterreadingoption'); + $cipherData = $('#cipherdata'); + $clearText = $('#cleartext'); + $cloneButton = $('#clonebutton'); + $clonedFile = $('#clonedfile'); + $comments = $('#comments'); + $discussion = $('#discussion'); + $errorMessage = $('#errormessage'); + $expiration = $('#expiration'); + $fileRemoveButton = $('#fileremovebutton'); + $fileWrap = $('#filewrap'); + $formatter = $('#formatter'); + $image = $('#image'); + $loadingIndicator = $('#loadingindicator'); + $message = $('#message'); + $messageEdit = $('#messageedit'); + $messagePreview = $('#messagepreview'); + $newButton = $('#newbutton'); + $openDisc = $('#opendisc'); + $openDiscussion = $('#opendiscussion'); + $password = $('#password'); + $passwordInput = $('#passwordinput'); + $passwordModal = $('#passwordmodal'); + $passwordForm = $('#passwordform'); + $passwordDecrypt = $('#passworddecrypt'); + $pasteResult = $('#pasteresult'); + // $pasteUrl is saved in sendDataContinue() if/after it is // actually created - this.prettyMessage = $('#prettymessage'); - this.prettyPrint = $('#prettyprint'); - this.preview = $('#preview'); - this.rawTextButton = $('#rawtextbutton'); - this.remainingTime = $('#remainingtime'); - this.sendButton = $('#sendbutton'); - this.status = $('#status'); - this.bindEvents(); + $prettyMessage = $('#prettymessage'); + $prettyPrint = $('#prettyprint'); + $preview = $('#preview'); + $rawTextButton = $('#rawtextbutton'); + $remainingTime = $('#remainingtime'); + // $replyStatus is saved in openReply() + $sendButton = $('#sendbutton'); + $status = $('#status'); + bindEvents(); // display status returned by php code, if any (eg. paste was properly deleted) - if (this.status.text().length > 0) + if ($status.text().length > 0) { - this.showStatus(this.status.text()); + me.showStatus($status.text()); return; } // keep line height even if content empty - this.status.html(' '); + $status.html(' '); // display an existing paste - if (this.cipherData.text().length > 1) + if ($cipherData.text().length > 1) { // missing decryption key in URL? if (window.location.hash.length === 0) { - this.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)')); + me.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)')); return; } // show proper elements on screen - this.stateExistingPaste(); - this.displayMessages(); + me.stateExistingPaste(); + me.displayMessages(); } // display error message from php code - else if (this.errorMessage.text().length > 1) + else if ($errorMessage.text().length > 1) { - this.showError(this.errorMessage.text()); + me.showError($errorMessage.text()); } // create a new paste else { - this.newPaste(); + me.newPaste(); } - } - } + }; + + return me; + })(window, document); /** * main application start, called when DOM is fully loaded and diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 60c6727..698d359 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + From 4e86da8f7207608fa2cea72ac78d0a35d7270623 Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 8 Feb 2017 13:54:37 +0100 Subject: [PATCH 02/59] Remove proxy Also I kept care to (fix?) the focus of the password input. It only works in an anonymous function for some reason. --- js/privatebin.js | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index ed3baa3..85c71b6 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -952,7 +952,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $place = $(cname); } - divComment.find('button').click({commentid: comment.id}, $.proxy(me.openReply, me)); + divComment.find('button').click({commentid: comment.id}, me.openReply); helper.setElementText(divCommentData, commenttext); helper.urls2links(divCommentData); @@ -986,7 +986,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { '
' ); - divComment.find('button').click({commentid: helper.pasteId()}, $.proxy(me.openReply, me)); + divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); $comments.append(divComment); $discussion.removeClass('hidden'); } @@ -1020,7 +1020,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ); reply.find('button').click( {parentid: commentid}, - $.proxy(me.sendComment, me) + me.sendComment ); source.after(reply); $replyStatus = $('#replystatus'); @@ -1240,11 +1240,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // save newly created element $pasteUrl = $('#pasteurl'); // and add click event - $pasteUrl.click($.proxy(me.pasteLinkClick, me)); + $pasteUrl.click(me.pasteLinkClick); var shortenButton = $('#shortenbutton'); if (shortenButton) { - shortenButton.click($.proxy(me.sendToShortener, me)); + shortenButton.click(me.sendToShortener); } $('#deletelink').html('' + i18n._('Delete data') + ''); $pasteResult.removeClass('hidden'); @@ -1830,31 +1830,34 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ function bindEvents() { - $burnAfterReading.change($.proxy(me.changeBurnAfterReading, me)); - $openDisc.change($.proxy(me.changeOpenDisc, me)); - $sendButton.click($.proxy(me.sendData, me)); - $cloneButton.click($.proxy(me.clonePaste, me)); - $rawTextButton.click($.proxy(me.rawText, me)); - $fileRemoveButton.click($.proxy(me.removeAttachment, me)); - $('.reloadlink').click($.proxy(me.reloadPage, me)); + $burnAfterReading.change(me.changeBurnAfterReading); + $openDisc.change(me.changeOpenDisc); + $sendButton.click(me.sendData); + $cloneButton.click(me.clonePaste); + $rawTextButton.click(me.rawText); + $fileRemoveButton.click(me.removeAttachment); + $('.reloadlink').click(me.reloadPage); $message.keydown(me.supportTabs); - $messageEdit.click($.proxy(me.viewEditor, me)); - $messagePreview.click($.proxy(me.viewPreview, me)); + $messageEdit.click(me.viewEditor); + $messagePreview.click(me.viewPreview); // bootstrap template drop downs - $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(me.setExpiration, me)); - $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(me.setFormat, me)); - $('#language ul.dropdown-menu li a').click($.proxy(me.setLanguage, me)); + $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration); + $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat); + $('#language ul.dropdown-menu li a').click(me.setLanguage); // page template drop down - $('#language select option').click($.proxy(me.setLanguage, me)); + $('#language select option').click(me.setLanguage); + // focus password input when it is shown + $passwordModal.on('shown.bs.modal', function () { + $passwordDecrypt.focus(); + }); // handle modal password request on decryption - $passwordModal.on('shown.bs.modal', $.proxy($passwordDecrypt.focus, me)); - $passwordModal.on('hidden.bs.modal', $.proxy(me.decryptPasswordModal, me)); - $passwordForm.submit($.proxy(me.submitPasswordModal, me)); + $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal); + $passwordForm.submit(me.submitPasswordModal); - $(window).on('popstate', $.proxy(me.historyChange, me)); + $(window).on('popstate', me.historyChange); }; /** From b01a28d5800e1c96a417d37306bd6a2d1ffed783 Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 8 Feb 2017 14:15:58 +0100 Subject: [PATCH 03/59] remove some more this, slightly change comments --- js/privatebin.js | 72 +++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 85c71b6..6322c0e 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -135,26 +135,25 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * set text of a DOM element (required for IE), + * set text of a jQuery element (required for IE), * * @name helper.setElementText * @function - * @param {Object} element - a DOM element + * @param {jQuery} $element - a jQuery element * @param {string} text - the text to enter - * @this is equivalent to element.text(text) * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look */ - me.setElementText = function(element, text) + me.setElementText = function($element, text) { // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this... if ($('#oldienotice').is(':visible')) { var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
'); - element.html('
' + html + '
'); + $element.html('
' + html + '
'); } // for other (sane) browsers: else { - element.text(text); + $element.text(text); } }; @@ -163,19 +162,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name helper.setMessage * @function - * @param {Object} element - a jQuery wrapped DOM element + * @param {jQuery} $element - a jQuery wrapped DOM element * @param {string} message - the message to append */ - me.setMessage = function(element, message) + me.setMessage = function($element, message) { - var content = element.contents(); + var content = $element.contents(); if (content.length > 0) { content[content.length - 1].nodeValue = ' ' + message; } else { - me.setElementText(element, message); + me.setElementText($element, message); } }; @@ -931,63 +930,68 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $comments.html(''); + var $divComment; + // iterate over comments for (var i = 0; i < paste.comments.length; ++i) { var $place = $comments, comment = paste.comments[i], - commenttext = filter.decipher(key, password, comment.data), - // if parent comment exists, display below (CSS will automatically shift it to the right) - cname = '#comment_' + comment.parentid, - divComment = $('
' - + '
' - + '
' - + '
'), - divCommentData = divComment.find('div.commentdata'); - - // if the element exists in page - if ($(cname).length) + commentText = filter.decipher(key, password, comment.data), + $parentComment = $('#comment_' + comment.parentid); + + $divComment = $('
' + + '
' + + '
' + + '
'); + var $divCommentData = $divComment.find('div.commentdata'); + + // if parent comment exists + if ($parentComment.length) { - $place = $(cname); + // shift comment to the right + $place = $parentComment; } - divComment.find('button').click({commentid: comment.id}, me.openReply); - helper.setElementText(divCommentData, commenttext); - helper.urls2links(divCommentData); + $divComment.find('button').click({commentid: comment.id}, me.openReply); + helper.setElementText($divCommentData, commentText); + helper.urls2links($divCommentData); // try to get optional nickname var nick = filter.decipher(key, password, comment.meta.nickname); if (nick.length > 0) { - divComment.find('span.nickname').text(nick); + $divComment.find('span.nickname').text(nick); } else { divComment.find('span.nickname').html('' + i18n._('Anonymous') + ''); } - divComment.find('span.commentdate') + $divComment.find('span.commentdate') .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')') .attr('title', 'CommentID: ' + comment.id); // if an avatar is available, display it if (comment.meta.vizhash) { - divComment.find('span.nickname') + $divComment.find('span.nickname') .before( ' ' ); } - $place.append(divComment); + $place.append($divComment); } - var divComment = $( + + // add 'add new comment' area + $divComment = $( '
' ); - divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); - $comments.append(divComment); + $divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); + $comments.append($divComment); $discussion.removeClass('hidden'); } }; From e84cfc58a16d56b2deb9450c5ca8033a9a4b9b37 Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 8 Feb 2017 20:11:04 +0100 Subject: [PATCH 04/59] JS: tried namespaces --- js/privatebin.js | 3488 +++++++++++++++++++++++----------------------- 1 file changed, 1742 insertions(+), 1746 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 6322c0e..adb26fd 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -25,1948 +25,1944 @@ // Immediately start random number generator collector. sjcl.random.startCollectors(); -// jQuery(document).ready(function() { -// // startup -// } +// startup +jQuery(document).ready(function() { + /** + * main application start, called when DOM is fully loaded and + * runs controller initalization after translations are loaded + */ + PrivateBin.i18n.loadTranslations(); +}); + +/** + * @name PrivateBin + * @namespace + */ +var PrivateBin = window.PrivateBin || {}; + +/** + * static helper methods + * + * @param {object} window + * @param {object} document + * @name helper + * @class + */ +PrivateBin.helper = (function (window, document, jQuery, sjcl, Base64, RawDeflate) { + var me = {}; -jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** - * static helper methods + * character to HTML entity lookup table * - * @param {object} window - * @param {object} document - * @name helper - * @class + * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} + * @private + * @enum {Object} + * @readonly */ - var helper = (function (window, document) { - var me = {}; - - /** - * character to HTML entity lookup table - * - * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} - * @private - * @enum {Object} - * @readonly - */ - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; - /** - * cache for script location - * - * @private - * @enum {string|null} - */ - var scriptLocation = null; - - /** - * converts a duration (in seconds) into human friendly approximation - * - * @name helper.secondsToHuman - * @function - * @param {number} seconds - * @return {Array} - */ - me.secondsToHuman = function(seconds) - { - var v; - if (seconds < 60) - { - v = Math.floor(seconds); - return [v, 'second']; - } - if (seconds < 60 * 60) - { - v = Math.floor(seconds / 60); - return [v, 'minute']; - } - if (seconds < 60 * 60 * 24) - { - v = Math.floor(seconds / (60 * 60)); - return [v, 'hour']; - } - // If less than 2 months, display in days: - if (seconds < 60 * 60 * 24 * 60) - { - v = Math.floor(seconds / (60 * 60 * 24)); - return [v, 'day']; - } - v = Math.floor(seconds / (60 * 60 * 24 * 30)); - return [v, 'month']; - }; + /** + * cache for script location + * + * @private + * @enum {string|null} + */ + var scriptLocation = null; - /** - * text range selection - * - * @see {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse} - * @name helper.selectText - * @function - * @param {HTMLElement} element - */ - me.selectText = function(element) + /** + * converts a duration (in seconds) into human friendly approximation + * + * @name helper.secondsToHuman + * @function + * @param {number} seconds + * @return {Array} + */ + me.secondsToHuman = function(seconds) + { + var v; + if (seconds < 60) + { + v = Math.floor(seconds); + return [v, 'second']; + } + if (seconds < 60 * 60) { - var range, selection; + v = Math.floor(seconds / 60); + return [v, 'minute']; + } + if (seconds < 60 * 60 * 24) + { + v = Math.floor(seconds / (60 * 60)); + return [v, 'hour']; + } + // If less than 2 months, display in days: + if (seconds < 60 * 60 * 24 * 60) + { + v = Math.floor(seconds / (60 * 60 * 24)); + return [v, 'day']; + } + v = Math.floor(seconds / (60 * 60 * 24 * 30)); + return [v, 'month']; + }; - // MS - if (document.body.createTextRange) - { - range = document.body.createTextRange(); - range.moveToElementText(element); - range.select(); - } - // all others - else if (window.getSelection) - { - selection = window.getSelection(); - range = document.createRange(); - range.selectNodeContents(element); - selection.removeAllRanges(); - selection.addRange(range); - } - }; + /** + * text range selection + * + * @see {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse} + * @name helper.selectText + * @function + * @param {HTMLElement} element + */ + me.selectText = function(element) + { + var range, selection; - /** - * set text of a jQuery element (required for IE), - * - * @name helper.setElementText - * @function - * @param {jQuery} $element - a jQuery element - * @param {string} text - the text to enter - * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look - */ - me.setElementText = function($element, text) + // MS + if (document.body.createTextRange) { - // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this... - if ($('#oldienotice').is(':visible')) { - var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
'); - $element.html('
' + html + '
'); - } - // for other (sane) browsers: - else - { - $element.text(text); - } - }; - - /** - * replace last child of element with message - * - * @name helper.setMessage - * @function - * @param {jQuery} $element - a jQuery wrapped DOM element - * @param {string} message - the message to append - */ - me.setMessage = function($element, message) + range = document.body.createTextRange(); + range.moveToElementText(element); + range.select(); + } + // all others + else if (window.getSelection) { - var content = $element.contents(); - if (content.length > 0) - { - content[content.length - 1].nodeValue = ' ' + message; - } - else - { - me.setElementText($element, message); - } - }; + selection = window.getSelection(); + range = document.createRange(); + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + } + }; - /** - * convert URLs to clickable links. - * URLs to handle: - *
-         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
-         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
-         * 
- * - * @name helper.urls2links - * @function - * @param {Object} element - a jQuery DOM element - */ - me.urls2links = function(element) + /** + * set text of a jQuery element (required for IE), + * + * @name helper.setElementText + * @function + * @param {jQuery} $element - a jQuery element + * @param {string} text - the text to enter + * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look + */ + me.setElementText = function($element, text) + { + // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this... + if ($('#oldienotice').is(':visible')) { + var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
'); + $element.html('
' + html + '
'); + } + // for other (sane) browsers: + else { - var markup = '$1'; - element.html( - element.html().replace( - /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, - markup - ) - ); - element.html( - element.html().replace( - /((magnet):[\w?=&.\/-;#@~%+-]+)/ig, - markup - ) - ); - }; + $element.text(text); + } + }; - /** - * minimal sprintf emulation for %s and %d formats - * - * @see {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914} - * @name helper.sprintf - * @function - * @param {string} format - * @param {...*} args - one or multiple parameters injected into format string - * @return {string} - */ - me.sprintf = function() + /** + * replace last child of element with message + * + * @name helper.setMessage + * @function + * @param {jQuery} $element - a jQuery wrapped DOM element + * @param {string} message - the message to append + */ + me.setMessage = function($element, message) + { + var content = $element.contents(); + if (content.length > 0) { - var args = arguments; - if (typeof arguments[0] === 'object') - { - args = arguments[0]; - } - var format = args[0], - i = 1; - return format.replace(/%((%)|s|d)/g, function (m) { - // m is the matched format, e.g. %s, %d - var val; - if (m[2]) { - val = m[2]; - } else { - val = args[i]; - // A switch statement so that the formatter can be extended. - switch (m) - { - case '%d': - val = parseFloat(val); - if (isNaN(val)) { - val = 0; - } - break; - default: - // Default is %s - } - ++i; - } - return val; - }); - }; + content[content.length - 1].nodeValue = ' ' + message; + } + else + { + me.setElementText($element, message); + } + }; - /** - * get value of cookie, if it was set, empty string otherwise - * - * @see {@link http://www.w3schools.com/js/js_cookies.asp} - * @name helper.getCookie - * @function - * @param {string} cname - * @return {string} - */ - me.getCookie = function(cname) { - var name = cname + '=', - ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; ++i) { - var c = ca[i]; - while (c.charAt(0) === ' ') - { - c = c.substring(1); - } - if (c.indexOf(name) === 0) + /** + * convert URLs to clickable links. + * URLs to handle: + *
+     *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
+     *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+     *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+     * 
+ * + * @name helper.urls2links + * @function + * @param {Object} element - a jQuery DOM element + */ + me.urls2links = function(element) + { + var markup = '$1'; + element.html( + element.html().replace( + /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, + markup + ) + ); + element.html( + element.html().replace( + /((magnet):[\w?=&.\/-;#@~%+-]+)/ig, + markup + ) + ); + }; + + /** + * minimal sprintf emulation for %s and %d formats + * + * @see {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914} + * @name helper.sprintf + * @function + * @param {string} format + * @param {...*} args - one or multiple parameters injected into format string + * @return {string} + */ + me.sprintf = function() + { + var args = arguments; + if (typeof arguments[0] === 'object') + { + args = arguments[0]; + } + var format = args[0], + i = 1; + return format.replace(/%((%)|s|d)/g, function (m) { + // m is the matched format, e.g. %s, %d + var val; + if (m[2]) { + val = m[2]; + } else { + val = args[i]; + // A switch statement so that the formatter can be extended. + switch (m) { - return c.substring(name.length, c.length); + case '%d': + val = parseFloat(val); + if (isNaN(val)) { + val = 0; + } + break; + default: + // Default is %s } + ++i; } - return ''; - }; + return val; + }); + }; - /** - * get the current script location (without search or hash part of the URL), - * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/ - * - * @name helper.scriptLocation - * @function - * @return {string} current script location - */ - me.scriptLocation = function() - { - // check for cached version - if (scriptLocation !== null) { - return scriptLocation; + /** + * get value of cookie, if it was set, empty string otherwise + * + * @see {@link http://www.w3schools.com/js/js_cookies.asp} + * @name helper.getCookie + * @function + * @param {string} cname + * @return {string} + */ + me.getCookie = function(cname) { + var name = cname + '=', + ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; ++i) { + var c = ca[i]; + while (c.charAt(0) === ' ') + { + c = c.substring(1); } - - 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) + if (c.indexOf(name) === 0) { - scriptLocation = scriptLocation.substring(0, hashIndex); + return c.substring(name.length, c.length); } + } + return ''; + }; + /** + * get the current script location (without search or hash part of the URL), + * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/ + * + * @name helper.scriptLocation + * @function + * @return {string} current script location + */ + me.scriptLocation = function() + { + // check for cached version + if (scriptLocation !== null) { return scriptLocation; - }; + } + + scriptLocation = window.location.href.substring( + 0, + window.location.href.length - window.location.search.length - window.location.hash.length + ); - /** - * get the pastes unique identifier from the URL, - * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487 - * - * @name helper.pasteId - * @function - * @return {string} unique identifier - */ - me.pasteId = function() + var hashIndex = scriptLocation.indexOf('?'); + + if (hashIndex !== -1) { - return window.location.search.substring(1); - }; + scriptLocation = scriptLocation.substring(0, hashIndex); + } + + return scriptLocation; + }; + + /** + * get the pastes unique identifier from the URL, + * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487 + * + * @name helper.pasteId + * @function + * @return {string} unique identifier + */ + me.pasteId = function() + { + return window.location.search.substring(1); + }; - /** - * return the deciphering key stored in anchor part of the URL - * - * @name helper.pageKey - * @function - * @return {string} key - */ - me.pageKey = function() + /** + * return the deciphering key stored in anchor part of the URL + * + * @name helper.pageKey + * @function + * @return {string} key + */ + me.pageKey = function() + { + var key = window.location.hash.substring(1), + i = key.indexOf('&'); + + // Some web 2.0 services and redirectors add data AFTER the anchor + // (such as &utm_source=...). We will strip any additional data. + if (i > -1) { - var key = window.location.hash.substring(1), - i = key.indexOf('&'); + key = key.substring(0, i); + } - // Some web 2.0 services and redirectors add data AFTER the anchor - // (such as &utm_source=...). We will strip any additional data. - if (i > -1) - { - key = key.substring(0, i); - } + return key; + }; - return key; - }; + /** + * convert all applicable characters to HTML entities + * + * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} + * @name helper.htmlEntities + * @function + * @param {string} str + * @return {string} escaped HTML + */ + me.htmlEntities = function(str) { + return String(str).replace( + /[&<>"'`=\/]/g, function(s) { + return entityMap[s]; + }); + }; - /** - * convert all applicable characters to HTML entities - * - * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} - * @name helper.htmlEntities - * @function - * @param {string} str - * @return {string} escaped HTML - */ - me.htmlEntities = function(str) { - return String(str).replace( - /[&<>"'`=\/]/g, function(s) { - return entityMap[s]; - }); - }; + return me; +})(window, document, jQuery, sjcl, Base64, RawDeflate); - return me; - })(window, document); +/** + * internationalization methods + * + * @param {object} window + * @param {object} document + * @name i18n + * @class + */ +PrivateBin.i18n = (function (window, document, jQuery, sjcl, Base64, RawDeflate) { + var me = {}; /** - * internationalization methods + * supported languages, minus the built in 'en' * - * @param {object} window - * @param {object} document - * @name i18n - * @class + * @private + * @prop {string[]} + * @readonly */ - var i18n = (function (window, document) { - var me = {}; - - /** - * supported languages, minus the built in 'en' - * - * @private - * @prop {string[]} - * @readonly - */ - var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh']; - - /** - * built in language - * - * @private - * @prop {string} - */ - var language = 'en'; - - /** - * translation cache - * - * @private - * @enum {Object} - */ - var translations = {}; - - /** - * translate a string, alias for i18n.translate() - * - * @name i18n._ - * @function - * @param {string} messageId - * @param {...*} args - one or multiple parameters injected into placeholders - * @return {string} - */ - me._ = function() - { - return me.translate(arguments); - }; + var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh']; + + /** + * built in language + * + * @private + * @prop {string} + */ + var language = 'en'; + + /** + * translation cache + * + * @private + * @enum {Object} + */ + var translations = {}; + + /** + * translate a string, alias for i18n.translate() + * + * @name i18n._ + * @function + * @param {string} messageId + * @param {...*} args - one or multiple parameters injected into placeholders + * @return {string} + */ + me._ = function() + { + return me.translate(arguments); + }; - /** - * translate a string - * - * @name i18n.translate - * @function - * @param {string} messageId - * @param {...*} args - one or multiple parameters injected into placeholders - * @return {string} - */ - me.translate = function() + /** + * translate a string + * + * @name i18n.translate + * @function + * @param {string} messageId + * @param {...*} args - one or multiple parameters injected into placeholders + * @return {string} + */ + me.translate = function() + { + var args = arguments, messageId; + if (typeof arguments[0] === 'object') { - var args = arguments, messageId; - if (typeof arguments[0] === 'object') - { - args = arguments[0]; - } - var usesPlurals = $.isArray(args[0]); - if (usesPlurals) - { - // use the first plural form as messageId, otherwise the singular - messageId = (args[0].length > 1 ? args[0][1] : args[0][0]); - } - else - { - messageId = args[0]; - } - if (messageId.length === 0) - { - return messageId; - } - if (!translations.hasOwnProperty(messageId)) - { - if (language !== 'en') - { - console.error( - 'Missing ' + language + ' translation for: ' + messageId - ); - } - translations[messageId] = args[0]; - } - if (usesPlurals && $.isArray(translations[messageId])) + args = arguments[0]; + } + var usesPlurals = $.isArray(args[0]); + if (usesPlurals) + { + // use the first plural form as messageId, otherwise the singular + messageId = (args[0].length > 1 ? args[0][1] : args[0][0]); + } + else + { + messageId = args[0]; + } + if (messageId.length === 0) + { + return messageId; + } + if (!translations.hasOwnProperty(messageId)) + { + if (language !== 'en') { - var n = parseInt(args[1] || 1, 10), - key = me.getPluralForm(n), - maxKey = translations[messageId].length - 1; - if (key > maxKey) - { - key = maxKey; - } - args[0] = translations[messageId][key]; - args[1] = n; + console.error( + 'Missing ' + language + ' translation for: ' + messageId + ); } - else + translations[messageId] = args[0]; + } + if (usesPlurals && $.isArray(translations[messageId])) + { + var n = parseInt(args[1] || 1, 10), + key = me.getPluralForm(n), + maxKey = translations[messageId].length - 1; + if (key > maxKey) { - args[0] = translations[messageId]; + key = maxKey; } - return helper.sprintf(args); - }; + args[0] = translations[messageId][key]; + args[1] = n; + } + else + { + args[0] = translations[messageId]; + } + return helper.sprintf(args); + }; - /** - * per language functions to use to determine the plural form - * - * @see {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html} - * @name i18n.getPluralForm - * @function - * @param {number} n - * @return {number} array key - */ - me.getPluralForm = function(n) { - switch (language) - { - case 'fr': - case 'oc': - case 'zh': - return (n > 1 ? 1 : 0); - case 'pl': - return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); - case 'ru': - return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); - case 'sl': - return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0))); - // de, en, es, it, no - default: - return (n !== 1 ? 1 : 0); - } - }; + /** + * per language functions to use to determine the plural form + * + * @see {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html} + * @name i18n.getPluralForm + * @function + * @param {number} n + * @return {number} array key + */ + me.getPluralForm = function(n) { + switch (language) + { + case 'fr': + case 'oc': + case 'zh': + return (n > 1 ? 1 : 0); + case 'pl': + return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); + case 'ru': + return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); + case 'sl': + return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0))); + // de, en, es, it, no + default: + return (n !== 1 ? 1 : 0); + } + }; + + /** + * load translations into cache, then trigger controller initialization + * + * @name i18n.loadTranslations + * @function + */ + me.loadTranslations = function() + { + var newLanguage = PrivateBin.helper.getCookie('lang'); - /** - * load translations into cache, then trigger controller initialization - * - * @name i18n.loadTranslations - * @function - */ - me.loadTranslations = function() + // auto-select language based on browser settings + if (newLanguage.length === 0) { - var newLanguage = helper.getCookie('lang'); + newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2); + } - // auto-select language based on browser settings - if (newLanguage.length === 0) - { - newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2); - } + // if language is already used (e.g, default 'en'), skip update + if (newLanguage === language) + { + controller.init(); + return; + } - // if language is already used (e.g, default 'en'), skip update - if (newLanguage === language) - { - controller.init(); - return; - } + // if language is not supported, show error + if (supportedLanguages.indexOf(newLanguage) === -1) + { + console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage); + controller.init(); + } - // if language is not supported, show error - if (supportedLanguages.indexOf(newLanguage) === -1) - { - console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage); - controller.init(); - } + // load strongs from JSON + $.getJSON('i18n/' + newLanguage + '.json', function(data) { + language = newLanguage; + translations = data; + }).fail(function (data, textStatus, errorMsg) { + console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); + }); - // load strongs from JSON - $.getJSON('i18n/' + newLanguage + '.json', function(data) { - language = newLanguage; - translations = data; - }).fail(function (data, textStatus, errorMsg) { - console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); - }); + controller.init(); + }; - controller.init(); - }; + return me; +})(window, document, jQuery, sjcl, Base64, RawDeflate); + +/** + * filter methods + * + * @param {object} window + * @param {object} document + * @name filter + * @class + */ +PrivateBin.filter = (function (window, document, jQuery, sjcl, Base64, RawDeflate) { + var me = {}; - return me; - })(window, document); + /** + * compress a message (deflate compression), returns base64 encoded data + * + * @name filter.compress + * @function + * @param {string} message + * @return {string} base64 data + */ + me.compress = function(message) + { + return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) ); + }; /** - * filter methods + * decompress a message compressed with filter.compress() * - * @param {object} window - * @param {object} document - * @name filter - * @class + * @name filter.decompress + * @function + * @param {string} data - base64 data + * @return {string} message */ - var filter = (function (window, document) { - var me = {}; - - /** - * compress a message (deflate compression), returns base64 encoded data - * - * @name filter.compress - * @function - * @param {string} message - * @return {string} base64 data - */ - me.compress = function(message) - { - return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) ); - }, - - /** - * decompress a message compressed with filter.compress() - * - * @name filter.decompress - * @function - * @param {string} data - base64 data - * @return {string} message - */ - me.decompress = function(data) + me.decompress = function(data) + { + return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) ); + }; + + /** + * compress, then encrypt message with given key and password + * + * @name filter.cipher + * @function + * @param {string} key + * @param {string} password + * @param {string} message + * @return {string} data - JSON with encrypted data + */ + me.cipher = function(key, password, message) + { + // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit + var options = {mode: 'gcm', ks: 256, ts: 128}; + if ((password || '').trim().length === 0) { - return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) ); - }, - - /** - * compress, then encrypt message with given key and password - * - * @name filter.cipher - * @function - * @param {string} key - * @param {string} password - * @param {string} message - * @return {string} data - JSON with encrypted data - */ - me.cipher = function(key, password, message) + return sjcl.encrypt(key, me.compress(message), options); + } + return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options); + }; + + /** + * decrypt message with key, then decompress + * + * @name filter.decipher + * @function + * @param {string} key + * @param {string} password + * @param {string} data - JSON with encrypted data + * @return {string} decrypted message + */ + me.decipher = function(key, password, data) + { + if (data !== undefined) { - // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit - var options = {mode: 'gcm', ks: 256, ts: 128}; - if ((password || '').trim().length === 0) + try { - return sjcl.encrypt(key, me.compress(message), options); + return me.decompress(sjcl.decrypt(key, data)); } - return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options); - }, - - /** - * decrypt message with key, then decompress - * - * @name filter.decipher - * @function - * @param {string} key - * @param {string} password - * @param {string} data - JSON with encrypted data - * @return {string} decrypted message - */ - me.decipher = function(key, password, data) - { - if (data !== undefined) + catch(err) { try { - return me.decompress(sjcl.decrypt(key, data)); + return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); } - catch(err) + catch(e) { - try - { - return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); - } - catch(e) - { - // ignore error, because ????? @TODO - } + // ignore error, because ????? @TODO } } - return ''; } + return ''; + }; - return me; - })(window, document); + return me; +})(window, document, jQuery, sjcl, Base64, RawDeflate); - /** - * PrivateBin logic +/** + * PrivateBin logic + * + * @param {object} window + * @param {object} document + * @name controller + * @class + */ +PrivateBin.controller = (function (window, document, jQuery, sjcl, Base64, RawDeflate) { + var me = {}; + + /** + * headers to send in AJAX requests * - * @param {object} window - * @param {object} document - * @name controller - * @class + * @private + * @enum {Object} */ - var controller = (function (window, document) { - var me = {}; - - /** - * headers to send in AJAX requests - * - * @private - * @enum {Object} - */ - var headers = {'X-Requested-With': 'JSONHttpRequest'}; - - /** - * URL shortners create address - * - * @private - * @prop {string} - */ - var shortenerUrl = ''; - - /** - * URL of newly created paste - * - * @private - * @prop {string} - */ - var createdPasteUrl = ''; - - // jQuery pre-loaded objects - var $attach, - $attachment, - $attachmentLink, - $burnAfterReading, - $burnAfterReadingOption, - $cipherData, - $clearText, - $cloneButton, - $clonedFile, - $comments, - $discussion, - $errorMessage, - $expiration, - $fileRemoveButton, - $fileWrap, - $formatter, - $image, - $loadingIndicator, - $message, - $messageEdit, - $messagePreview, - $newButton, - $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear - $openDiscussion, - $password, - $passwordInput, - $passwordModal, - $passwordForm, - $passwordDecrypt, - $pasteResult, - $pasteUrl, - $prettyMessage, - $prettyPrint, - $preview, - $rawTextButton, - $remainingTime, - $replyStatus, - $sendButton, - $status; - - /** - * ask the user for the password and set it - * - * @name controller.requestPassword - * @function - */ - me.requestPassword = function() - { - if ($passwordModal.length === 0) { - var password = prompt(i18n._('Please enter the password for this paste:'), ''); - if (password === null) - { - throw 'password prompt canceled'; - } - if (password.length === 0) - { - // recursive… - me.requestPassword(); - } else { - $passwordInput.val(password); - me.displayMessages(); - } + var headers = {'X-Requested-With': 'JSONHttpRequest'}; + + /** + * URL shortners create address + * + * @private + * @prop {string} + */ + var shortenerUrl = ''; + + /** + * URL of newly created paste + * + * @private + * @prop {string} + */ + var createdPasteUrl = ''; + + // jQuery pre-loaded objects + var $attach, + $attachment, + $attachmentLink, + $burnAfterReading, + $burnAfterReadingOption, + $cipherData, + $clearText, + $cloneButton, + $clonedFile, + $comments, + $discussion, + $errorMessage, + $expiration, + $fileRemoveButton, + $fileWrap, + $formatter, + $image, + $loadingIndicator, + $message, + $messageEdit, + $messagePreview, + $newButton, + $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear + $openDiscussion, + $password, + $passwordInput, + $passwordModal, + $passwordForm, + $passwordDecrypt, + $pasteResult, + $pasteUrl, + $prettyMessage, + $prettyPrint, + $preview, + $rawTextButton, + $remainingTime, + $replyStatus, + $sendButton, + $status; + + /** + * ask the user for the password and set it + * + * @name controller.requestPassword + * @function + */ + me.requestPassword = function() + { + if ($passwordModal.length === 0) { + var password = prompt(i18n._('Please enter the password for this paste:'), ''); + if (password === null) + { + throw 'password prompt canceled'; + } + if (password.length === 0) + { + // recursive… + me.requestPassword(); } else { - $passwordModal.modal(); + $passwordInput.val(password); + me.displayMessages(); } - }; + } else { + $passwordModal.modal(); + } + }; - /** - * use given format on paste, defaults to plain text - * - * @name controller.formatPaste - * @function - * @param {string} format - * @param {string} text - */ - me.formatPaste = function(format, text) - { - helper.setElementText($clearText, text); - helper.setElementText($prettyPrint, text); - - switch (format || 'plaintext') { - case 'markdown': - // silently fail if showdown is not available - // @TODO: maybe better show an error message? At least a warning? - if (typeof showdown === 'object') - { - var converter = new showdown.Converter({ - strikethrough: true, - tables: true, - tablesHeaderId: true - }); - $clearText.html( - converter.makeHtml(text) - ); - // add table classes from bootstrap css - $clearText.find('table').addClass('table-condensed table-bordered'); + /** + * use given format on paste, defaults to plain text + * + * @name controller.formatPaste + * @function + * @param {string} format + * @param {string} text + */ + me.formatPaste = function(format, text) + { + helper.setElementText($clearText, text); + helper.setElementText($prettyPrint, text); + + switch (format || 'plaintext') { + case 'markdown': + // silently fail if showdown is not available + // @TODO: maybe better show an error message? At least a warning? + if (typeof showdown === 'object') + { + var converter = new showdown.Converter({ + strikethrough: true, + tables: true, + tablesHeaderId: true + }); + $clearText.html( + converter.makeHtml(text) + ); + // add table classes from bootstrap css + $clearText.find('table').addClass('table-condensed table-bordered'); - $clearText.removeClass('hidden'); - } else { - console.error('showdown is not loaded, could not parse Markdown'); - } - $prettyMessage.addClass('hidden'); - break; - case 'syntaxhighlighting': - // silently fail if prettyprint is not available - // @TODO: maybe better show an error message? At least a warning? - if (typeof prettyPrintOne === 'function') + $clearText.removeClass('hidden'); + } else { + console.error('showdown is not loaded, could not parse Markdown'); + } + $prettyMessage.addClass('hidden'); + break; + case 'syntaxhighlighting': + // silently fail if prettyprint is not available + // @TODO: maybe better show an error message? At least a warning? + if (typeof prettyPrintOne === 'function') + { + if (typeof prettyPrint === 'function') { - if (typeof prettyPrint === 'function') - { - prettyPrint(); - } - $prettyPrint.html( - prettyPrintOne( - helper.htmlEntities(text), null, true - ) - ); - } else { - console.error('pretty print is not loaded, could not link '); + prettyPrint(); } - // fall through, as the rest is the same - default: // = 'plaintext' - // convert URLs to clickable links - helper.urls2links($clearText); - helper.urls2links($prettyPrint); - $clearText.addClass('hidden'); + $prettyPrint.html( + prettyPrintOne( + helper.htmlEntities(text), null, true + ) + ); + } else { + console.error('pretty print is not loaded, could not link '); + } + // fall through, as the rest is the same + default: // = 'plaintext' + // convert URLs to clickable links + helper.urls2links($clearText); + helper.urls2links($prettyPrint); + $clearText.addClass('hidden'); - $prettyPrint.css('white-space', 'pre-wrap'); - $prettyPrint.css('word-break', 'normal'); - $prettyPrint.removeClass('prettyprint'); + $prettyPrint.css('white-space', 'pre-wrap'); + $prettyPrint.css('word-break', 'normal'); + $prettyPrint.removeClass('prettyprint'); - $prettyMessage.removeClass('hidden'); - } - }; + $prettyMessage.removeClass('hidden'); + } + }; - /** - * show decrypted text in the display area, including discussion (if open) - * - * @name controller.displayMessages - * @function - * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) - */ - me.displayMessages = function(paste) - { - paste = paste || $.parseJSON($cipherData.text()); - var key = helper.pageKey(), - password = $passwordInput.val(); - if (!$prettyPrint.hasClass('prettyprinted')) { - // Try to decrypt the paste. - try + /** + * show decrypted text in the display area, including discussion (if open) + * + * @name controller.displayMessages + * @function + * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) + */ + me.displayMessages = function(paste) + { + paste = paste || $.parseJSON($cipherData.text()); + var key = helper.pageKey(), + password = $passwordInput.val(); + if (!$prettyPrint.hasClass('prettyprinted')) { + // Try to decrypt the paste. + try + { + if (paste.attachment) { - if (paste.attachment) + var attachment = filter.decipher(key, password, paste.attachment); + if (attachment.length === 0) { - var attachment = filter.decipher(key, password, paste.attachment); - if (attachment.length === 0) - { - if (password.length === 0) - { - me.requestPassword(); - return; - } - attachment = filter.decipher(key, password, paste.attachment); - } - if (attachment.length === 0) - { - throw 'failed to decipher attachment'; - } - - if (paste.attachmentname) - { - var attachmentname = filter.decipher(key, password, paste.attachmentname); - if (attachmentname.length > 0) - { - $attachmentLink.attr('download', attachmentname); - } - } - $attachmentLink.attr('href', attachment); - $attachment.removeClass('hidden'); - - // if the attachment is an image, display it - var imagePrefix = 'data:image/'; - if (attachment.substring(0, imagePrefix.length) === imagePrefix) + if (password.length === 0) { - $image.html( - $(document.createElement('img')) - .attr('src', attachment) - .attr('class', 'img-thumbnail') - ); - $image.removeClass('hidden'); + me.requestPassword(); + return; } + attachment = filter.decipher(key, password, paste.attachment); } - var cleartext = filter.decipher(key, password, paste.data); - if (cleartext.length === 0 && password.length === 0 && !paste.attachment) + if (attachment.length === 0) { - me.requestPassword(); - return; + throw 'failed to decipher attachment'; } - if (cleartext.length === 0 && !paste.attachment) + + if (paste.attachmentname) { - throw 'failed to decipher message'; + var attachmentname = filter.decipher(key, password, paste.attachmentname); + if (attachmentname.length > 0) + { + $attachmentLink.attr('download', attachmentname); + } } + $attachmentLink.attr('href', attachment); + $attachment.removeClass('hidden'); - $passwordInput.val(password); - if (cleartext.length > 0) + // if the attachment is an image, display it + var imagePrefix = 'data:image/'; + if (attachment.substring(0, imagePrefix.length) === imagePrefix) { - $('#pasteFormatter').val(paste.meta.formatter); - me.formatPaste(paste.meta.formatter, cleartext); + $image.html( + $(document.createElement('img')) + .attr('src', attachment) + .attr('class', 'img-thumbnail') + ); + $image.removeClass('hidden'); } } - catch(err) + var cleartext = filter.decipher(key, password, paste.data); + if (cleartext.length === 0 && password.length === 0 && !paste.attachment) { - me.stateOnlyNewPaste(); - me.showError(i18n._('Could not decrypt data (Wrong key?)')); + me.requestPassword(); return; } - } - - // display paste expiration / for your eyes only - if (paste.meta.expire_date) - { - var expiration = helper.secondsToHuman(paste.meta.remaining_time), - expirationLabel = [ - 'This document will expire in %d ' + expiration[1] + '.', - 'This document will expire in %d ' + expiration[1] + 's.' - ]; - helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0])); - $remainingTime.removeClass('foryoureyesonly') - .removeClass('hidden'); - } - if (paste.meta.burnafterreading) - { - // unfortunately many web servers don't support DELETE (and PUT) out of the box - $.ajax({ - type: 'POST', - url: helper.scriptLocation() + '?' + helper.pasteId(), - data: {deletetoken: 'burnafterreading'}, - dataType: 'json', - headers: headers - }) - .fail(function() { - controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); - }); - helper.setMessage($remainingTime, i18n._( - 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' - )); - $remainingTime.addClass('foryoureyesonly') - .removeClass('hidden'); - // discourage cloning (as it can't really be prevented) - $cloneButton.addClass('hidden'); - } - - // if the discussion is opened on this paste, display it - if (paste.meta.opendiscussion) - { - $comments.html(''); - - var $divComment; - - // iterate over comments - for (var i = 0; i < paste.comments.length; ++i) + if (cleartext.length === 0 && !paste.attachment) { - var $place = $comments, - comment = paste.comments[i], - commentText = filter.decipher(key, password, comment.data), - $parentComment = $('#comment_' + comment.parentid); - - $divComment = $('
' - + '
' - + '
' - + '
'); - var $divCommentData = $divComment.find('div.commentdata'); - - // if parent comment exists - if ($parentComment.length) - { - // shift comment to the right - $place = $parentComment; - } - $divComment.find('button').click({commentid: comment.id}, me.openReply); - helper.setElementText($divCommentData, commentText); - helper.urls2links($divCommentData); - - // try to get optional nickname - var nick = filter.decipher(key, password, comment.meta.nickname); - if (nick.length > 0) - { - $divComment.find('span.nickname').text(nick); - } - else - { - divComment.find('span.nickname').html('' + i18n._('Anonymous') + ''); - } - $divComment.find('span.commentdate') - .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')') - .attr('title', 'CommentID: ' + comment.id); - - // if an avatar is available, display it - if (comment.meta.vizhash) - { - $divComment.find('span.nickname') - .before( - ' ' - ); - } - - $place.append($divComment); + throw 'failed to decipher message'; } - // add 'add new comment' area - $divComment = $( - '
' - ); - $divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); - $comments.append($divComment); - $discussion.removeClass('hidden'); + $passwordInput.val(password); + if (cleartext.length > 0) + { + $('#pasteFormatter').val(paste.meta.formatter); + me.formatPaste(paste.meta.formatter, cleartext); + } } - }; - - /** - * open the comment entry when clicking the "Reply" button of a comment - * - * @name controller.openReply - * @function - * @param {Event} event - */ - me.openReply = function(event) - { - event.preventDefault(); - - // remove any other reply area - $('div.reply').remove(); - - var source = $(event.target), - commentid = event.data.commentid, - hint = i18n._('Optional nickname...'), - reply = $( - '

' + - '
' - ); - reply.find('button').click( - {parentid: commentid}, - me.sendComment - ); - source.after(reply); - $replyStatus = $('#replystatus'); - $('#replymessage').focus(); - }; - - /** - * send a reply in a discussion - * - * @name controller.sendComment - * @function - * @param {Event} event - */ - me.sendComment = function(event) - { - event.preventDefault(); - $errorMessage.addClass('hidden'); - // do not send if no data - var replyMessage = $('#replymessage'); - if (replyMessage.val().length === 0) + catch(err) { + me.stateOnlyNewPaste(); + me.showError(i18n._('Could not decrypt data (Wrong key?)')); return; } + } - me.showStatus(i18n._('Sending comment...'), true); - var parentid = event.data.parentid, - key = helper.pageKey(), - cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), - ciphernickname = '', - nick = $('#nickname').val(); - if (nick.length > 0) - { - ciphernickname = filter.cipher(key, $passwordInput.val(), nick); - } - var data_to_send = { - data: cipherdata, - parentid: parentid, - pasteid: helper.pasteId(), - nickname: ciphernickname - }; - + // display paste expiration / for your eyes only + if (paste.meta.expire_date) + { + var expiration = helper.secondsToHuman(paste.meta.remaining_time), + expirationLabel = [ + 'This document will expire in %d ' + expiration[1] + '.', + 'This document will expire in %d ' + expiration[1] + 's.' + ]; + helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0])); + $remainingTime.removeClass('foryoureyesonly') + .removeClass('hidden'); + } + if (paste.meta.burnafterreading) + { + // unfortunately many web servers don't support DELETE (and PUT) out of the box $.ajax({ type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, + url: helper.scriptLocation() + '?' + helper.pasteId(), + data: {deletetoken: 'burnafterreading'}, dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) - { - controller.showStatus(i18n._('Comment posted.')); - $.ajax({ - type: 'GET', - url: helper.scriptLocation() + '?' + helper.pasteId(), - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) - { - controller.displayMessages(data); - } - else if (data.status === 1) - { - controller.showError(i18n._('Could not refresh display: %s', data.message)); - } - else - { - controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); - } - } - }) - .fail(function() { - controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); - }); - } - else if (data.status === 1) - { - controller.showError(i18n._('Could not post comment: %s', data.message)); - } - else - { - controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); - } - } + headers: headers }) .fail(function() { - controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); + controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }); - }; + helper.setMessage($remainingTime, i18n._( + 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' + )); + $remainingTime.addClass('foryoureyesonly') + .removeClass('hidden'); + // discourage cloning (as it can't really be prevented) + $cloneButton.addClass('hidden'); + } - /** - * send a new paste to server - * - * @name controller.sendData - * @function - * @param {Event} event - */ - me.sendData = function(event) + // if the discussion is opened on this paste, display it + if (paste.meta.opendiscussion) { - event.preventDefault(); - var file = document.getElementById('file'), - files = (file && file.files) ? file.files : null; // FileList object - - // do not send if no data. - if ($message.val().length === 0 && !(files && files[0])) - { - return; - } - - // if sjcl has not collected enough entropy yet, display a message - if (!sjcl.random.isReady()) - { - me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); - sjcl.random.addEventListener('seeded', function() { - me.sendData(event); - }); - return; - } + $comments.html(''); - $('.navbar-toggle').click(); - $password.addClass('hidden'); - me.showStatus(i18n._('Sending paste...'), true); + var $divComment; - me.stateSubmittingPaste(); - - var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), - password = $passwordInput.val(); - if(files && files[0]) + // iterate over comments + for (var i = 0; i < paste.comments.length; ++i) { - if(typeof FileReader === undefined) + var $place = $comments, + comment = paste.comments[i], + commentText = filter.decipher(key, password, comment.data), + $parentComment = $('#comment_' + comment.parentid); + + $divComment = $('
' + + '
' + + '
' + + '
'); + var $divCommentData = $divComment.find('div.commentdata'); + + // if parent comment exists + if ($parentComment.length) { - // revert loading status… - me.stateNewPaste(); - me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); - return; + // shift comment to the right + $place = $parentComment; } - var reader = new FileReader(); - // closure to capture the file information - reader.onload = (function(theFile) - { - return function(e) { - controller.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, e.target.result), - filter.cipher(randomkey, password, theFile.name) - ); - }; - })(files[0]); - reader.readAsDataURL(files[0]); - } - else if($attachmentLink.attr('href')) - { - me.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, $attachmentLink.attr('href')), - $attachmentLink.attr('download') - ); - } - else - { - me.sendDataContinue(randomkey, '', ''); - } - }; + $divComment.find('button').click({commentid: comment.id}, me.openReply); + helper.setElementText($divCommentData, commentText); + helper.urls2links($divCommentData); - /** - * send a new paste to server, step 2 - * - * @name controller.sendDataContinue - * @function - * @param {string} randomkey - * @param {string} cipherdata_attachment - * @param {string} cipherdata_attachment_name - */ - me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) - { - var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), - data_to_send = { - data: cipherdata, - expire: $('#pasteExpiration').val(), - formatter: $('#pasteFormatter').val(), - burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, - opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 - }; - if (cipherdata_attachment.length > 0) - { - data_to_send.attachment = cipherdata_attachment; - if (cipherdata_attachment_name.length > 0) + // try to get optional nickname + var nick = filter.decipher(key, password, comment.meta.nickname); + if (nick.length > 0) { - data_to_send.attachmentname = cipherdata_attachment_name; + $divComment.find('span.nickname').text(nick); } - } - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) + else { - if (data.status === 0) { - me.stateExistingPaste(); - var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, - deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - me.showStatus(''); - $errorMessage.addClass('hidden'); - // show new URL in browser bar - history.pushState({type: 'newpaste'}, document.title, url); - - $('#pastelink').html( - i18n._( - 'Your paste is %s (Hit [Ctrl]+[c] to copy)', - url, url - ) + me.shortenUrl(url) - ); - // save newly created element - $pasteUrl = $('#pasteurl'); - // and add click event - $pasteUrl.click(me.pasteLinkClick); - - var shortenButton = $('#shortenbutton'); - if (shortenButton) { - shortenButton.click(me.sendToShortener); - } - $('#deletelink').html('' + i18n._('Delete data') + ''); - $pasteResult.removeClass('hidden'); - // we pre-select the link so that the user only has to [Ctrl]+[c] the link - helper.selectText($pasteUrl[0]); - me.showStatus(''); - me.formatPaste(data_to_send.formatter, $message.val()); - } - else if (data.status === 1) - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', data.message)); - } - else - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); - } + divComment.find('span.nickname').html('' + i18n._('Anonymous') + ''); } - }) - .fail(function() - { - // revert loading status… - me.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); - }); - }; + $divComment.find('span.commentdate') + .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')') + .attr('title', 'CommentID: ' + comment.id); - /** - * check if a URL shortener was defined and create HTML containing a link to it - * - * @name controller.shortenUrl - * @function - * @param {string} url - * @return {string} html - */ - me.shortenUrl = function(url) - { - var shortenerHtml = $('#shortenbutton'); - if (shortenerHtml) { - shortenerUrl = shortenerHtml.data('shortener'); - createdPasteUrl = url; - return ' ' + $('
').append(shortenerHtml.clone()).html(); - } - return ''; - }; + // if an avatar is available, display it + if (comment.meta.vizhash) + { + $divComment.find('span.nickname') + .before( + ' ' + ); + } - /** - * put the screen in "New paste" mode - * - * @name controller.stateNewPaste - * @function - */ - me.stateNewPaste = function() - { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - $sendButton.removeClass('hidden'); - $expiration.removeClass('hidden'); - $formatter.removeClass('hidden'); - $burnAfterReadingOption.removeClass('hidden'); - $openDisc.removeClass('hidden'); - $newButton.removeClass('hidden'); - $password.removeClass('hidden'); - $attach.removeClass('hidden'); - $message.removeClass('hidden'); - $preview.removeClass('hidden'); - $message.focus(); - }; + $place.append($divComment); + } - /** - * put the screen in mode after submitting a paste - * - * @name controller.stateSubmittingPaste - * @function - */ - me.stateSubmittingPaste = function() - { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $sendButton.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $newButton.addClass('hidden'); - $password.addClass('hidden'); - $attach.addClass('hidden'); - $message.addClass('hidden'); - $preview.addClass('hidden'); + // add 'add new comment' area + $divComment = $( + '
' + ); + $divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); + $comments.append($divComment); + $discussion.removeClass('hidden'); + } + }; - $loadingIndicator.removeClass('hidden'); - }; + /** + * open the comment entry when clicking the "Reply" button of a comment + * + * @name controller.openReply + * @function + * @param {Event} event + */ + me.openReply = function(event) + { + event.preventDefault(); + + // remove any other reply area + $('div.reply').remove(); + + var source = $(event.target), + commentid = event.data.commentid, + hint = i18n._('Optional nickname...'), + reply = $( + '

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

' + + '
' + ); + reply.find('button').click( + {parentid: commentid}, + me.sendComment + ); + source.after(reply); + $replyStatus = $('#replystatus'); + $('#replymessage').focus(); + }; + + /** + * send a reply in a discussion + * + * @name controller.sendComment + * @function + * @param {Event} event + */ + me.sendComment = function(event) + { + event.preventDefault(); + $errorMessage.addClass('hidden'); + // do not send if no data + var replyMessage = $('#replymessage'); + if (replyMessage.val().length === 0) { - me.stateOnlyNewPaste(); - me.showError(i18n._('Could not decrypt data (Wrong key?)')); return; } - } - // display paste expiration / for your eyes only - if (paste.meta.expire_date) - { - var expiration = helper.secondsToHuman(paste.meta.remaining_time), - expirationLabel = [ - 'This document will expire in %d ' + expiration[1] + '.', - 'This document will expire in %d ' + expiration[1] + 's.' - ]; - helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0])); - $remainingTime.removeClass('foryoureyesonly') - .removeClass('hidden'); - } - if (paste.meta.burnafterreading) - { - // unfortunately many web servers don't support DELETE (and PUT) out of the box + me.showStatus(i18n._('Sending comment...'), true); + var parentid = event.data.parentid, + key = helper.pageKey(), + cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), + ciphernickname = '', + nick = $('#nickname').val(); + if (nick.length > 0) + { + ciphernickname = filter.cipher(key, $passwordInput.val(), nick); + } + var data_to_send = { + data: cipherdata, + parentid: parentid, + pasteid: helper.pasteId(), + nickname: ciphernickname + }; + $.ajax({ type: 'POST', - url: helper.scriptLocation() + '?' + helper.pasteId(), - data: {deletetoken: 'burnafterreading'}, + url: helper.scriptLocation(), + data: data_to_send, dataType: 'json', - headers: headers + headers: headers, + success: function(data) + { + if (data.status === 0) + { + controller.showStatus(i18n._('Comment posted.')); + $.ajax({ + type: 'GET', + url: helper.scriptLocation() + '?' + helper.pasteId(), + dataType: 'json', + headers: headers, + success: function(data) + { + if (data.status === 0) + { + controller.displayMessages(data); + } + else if (data.status === 1) + { + controller.showError(i18n._('Could not refresh display: %s', data.message)); + } + else + { + controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); + } + } + }) + .fail(function() { + controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); + }); + } + else if (data.status === 1) + { + controller.showError(i18n._('Could not post comment: %s', data.message)); + } + else + { + controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); + } + } }) .fail(function() { - controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); + controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); }); - helper.setMessage($remainingTime, i18n._( - 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' - )); - $remainingTime.addClass('foryoureyesonly') - .removeClass('hidden'); - // discourage cloning (as it can't really be prevented) - $cloneButton.addClass('hidden'); - } + }; - // if the discussion is opened on this paste, display it - if (paste.meta.opendiscussion) + /** + * send a new paste to server + * + * @name controller.sendData + * @function + * @param {Event} event + */ + me.sendData = function(event) { - $comments.html(''); + event.preventDefault(); + var file = document.getElementById('file'), + files = (file && file.files) ? file.files : null; // FileList object - var $divComment; + // do not send if no data. + if ($message.val().length === 0 && !(files && files[0])) + { + return; + } - // iterate over comments - for (var i = 0; i < paste.comments.length; ++i) + // if sjcl has not collected enough entropy yet, display a message + if (!sjcl.random.isReady()) { - var $place = $comments, - comment = paste.comments[i], - commentText = filter.decipher(key, password, comment.data), - $parentComment = $('#comment_' + comment.parentid); - - $divComment = $('
' - + '
' - + '
' - + '
'); - var $divCommentData = $divComment.find('div.commentdata'); - - // if parent comment exists - if ($parentComment.length) - { - // shift comment to the right - $place = $parentComment; - } - $divComment.find('button').click({commentid: comment.id}, me.openReply); - helper.setElementText($divCommentData, commentText); - helper.urls2links($divCommentData); + me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); + sjcl.random.addEventListener('seeded', function() { + me.sendData(event); + }); + return; + } + + $('.navbar-toggle').click(); + $password.addClass('hidden'); + me.showStatus(i18n._('Sending paste...'), true); - // try to get optional nickname - var nick = filter.decipher(key, password, comment.meta.nickname); - if (nick.length > 0) + me.stateSubmittingPaste(); + + var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), + password = $passwordInput.val(); + if(files && files[0]) + { + if(typeof FileReader === undefined) { - $divComment.find('span.nickname').text(nick); + // revert loading status… + me.stateNewPaste(); + me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + return; } - else + var reader = new FileReader(); + // closure to capture the file information + reader.onload = (function(theFile) { - divComment.find('span.nickname').html('' + i18n._('Anonymous') + ''); - } - $divComment.find('span.commentdate') - .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')') - .attr('title', 'CommentID: ' + comment.id); + return function(e) { + controller.sendDataContinue( + randomkey, + filter.cipher(randomkey, password, e.target.result), + filter.cipher(randomkey, password, theFile.name) + ); + }; + })(files[0]); + reader.readAsDataURL(files[0]); + } + else if($attachmentLink.attr('href')) + { + me.sendDataContinue( + randomkey, + filter.cipher(randomkey, password, $attachmentLink.attr('href')), + $attachmentLink.attr('download') + ); + } + else + { + me.sendDataContinue(randomkey, '', ''); + } + }; - // if an avatar is available, display it - if (comment.meta.vizhash) + /** + * send a new paste to server, step 2 + * + * @name controller.sendDataContinue + * @function + * @param {string} randomkey + * @param {string} cipherdata_attachment + * @param {string} cipherdata_attachment_name + */ + me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) + { + var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), + data_to_send = { + data: cipherdata, + expire: $('#pasteExpiration').val(), + formatter: $('#pasteFormatter').val(), + burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, + opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 + }; + if (cipherdata_attachment.length > 0) + { + data_to_send.attachment = cipherdata_attachment; + if (cipherdata_attachment_name.length > 0) { - $divComment.find('span.nickname') - .before( - ' ' - ); + data_to_send.attachmentname = cipherdata_attachment_name; } - - $place.append($divComment); } + $.ajax({ + type: 'POST', + url: helper.scriptLocation(), + data: data_to_send, + dataType: 'json', + headers: headers, + success: function(data) + { + if (data.status === 0) { + me.stateExistingPaste(); + var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, + deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; + me.showStatus(''); + $errorMessage.addClass('hidden'); + // show new URL in browser bar + history.pushState({type: 'newpaste'}, document.title, url); + + $('#pastelink').html( + i18n._( + 'Your paste is %s (Hit [Ctrl]+[c] to copy)', + url, url + ) + me.shortenUrl(url) + ); + // save newly created element + $pasteUrl = $('#pasteurl'); + // and add click event + $pasteUrl.click(me.pasteLinkClick); + + var shortenButton = $('#shortenbutton'); + if (shortenButton) { + shortenButton.click(me.sendToShortener); + } + $('#deletelink').html('' + i18n._('Delete data') + ''); + $pasteResult.removeClass('hidden'); + // we pre-select the link so that the user only has to [Ctrl]+[c] the link + helper.selectText($pasteUrl[0]); + me.showStatus(''); + me.formatPaste(data_to_send.formatter, $message.val()); + } + else if (data.status === 1) + { + // revert loading status… + controller.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', data.message)); + } + else + { + // revert loading status… + controller.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); + } + } + }) + .fail(function() + { + // revert loading status… + me.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); + }); + }; - // add 'add new comment' area - $divComment = $( - '
' - ); - $divComment.find('button').click({commentid: helper.pasteId()}, me.openReply); - $comments.append($divComment); - $discussion.removeClass('hidden'); - } - }; + /** + * check if a URL shortener was defined and create HTML containing a link to it + * + * @name controller.shortenUrl + * @function + * @param {string} url + * @return {string} html + */ + me.shortenUrl = function(url) + { + var shortenerHtml = $('#shortenbutton'); + if (shortenerHtml) { + shortenerUrl = shortenerHtml.data('shortener'); + createdPasteUrl = url; + return ' ' + $('
').append(shortenerHtml.clone()).html(); + } + return ''; + }; - /** - * open the comment entry when clicking the "Reply" button of a comment - * - * @name controller.openReply - * @function - * @param {Event} event - */ - me.openReply = function(event) - { - event.preventDefault(); - - // remove any other reply area - $('div.reply').remove(); - - var source = $(event.target), - commentid = event.data.commentid, - hint = i18n._('Optional nickname...'), - reply = $( - '

' + - '
' - ); - reply.find('button').click( - {parentid: commentid}, - me.sendComment - ); - source.after(reply); - $replyStatus = $('#replystatus'); - $('#replymessage').focus(); - }; + /** + * put the screen in "New paste" mode + * + * @name controller.stateNewPaste + * @function + */ + me.stateNewPaste = function() + { + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + $sendButton.removeClass('hidden'); + $expiration.removeClass('hidden'); + $formatter.removeClass('hidden'); + $burnAfterReadingOption.removeClass('hidden'); + $openDisc.removeClass('hidden'); + $newButton.removeClass('hidden'); + $password.removeClass('hidden'); + $attach.removeClass('hidden'); + $message.removeClass('hidden'); + $preview.removeClass('hidden'); + $message.focus(); + }; - /** - * send a reply in a discussion - * - * @name controller.sendComment - * @function - * @param {Event} event - */ - me.sendComment = function(event) - { - event.preventDefault(); - $errorMessage.addClass('hidden'); - // do not send if no data - var replyMessage = $('#replymessage'); - if (replyMessage.val().length === 0) + /** + * put the screen in mode after submitting a paste + * + * @name controller.stateSubmittingPaste + * @function + */ + me.stateSubmittingPaste = function() { - return; - } + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $sendButton.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $newButton.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); + $preview.addClass('hidden'); - me.showStatus(i18n._('Sending comment...'), true); - var parentid = event.data.parentid, - key = helper.pageKey(), - cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), - ciphernickname = '', - nick = $('#nickname').val(); - if (nick.length > 0) + $loadingIndicator.removeClass('hidden'); + }; + + /** + * put the screen in a state where the only option is to submit a + * new paste + * + * @name controller.stateOnlyNewPaste + * @function + */ + me.stateOnlyNewPaste = function() { - ciphernickname = filter.cipher(key, $passwordInput.val(), nick); - } - var data_to_send = { - data: cipherdata, - parentid: parentid, - pasteid: helper.pasteId(), - nickname: ciphernickname + $message.text(''); + $attachment.addClass('hidden'); + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + $remainingTime.addClass('hidden'); + $pasteResult.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $sendButton.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); + $preview.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + + $newButton.removeClass('hidden'); }; - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) + /** + * put the screen in "Existing paste" mode + * + * @name controller.stateExistingPaste + * @function + * @param {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false + */ + me.stateExistingPaste = function(preview) + { + preview = preview || false; + + if (!preview) { - if (data.status === 0) - { - controller.showStatus(i18n._('Comment posted.')); - $.ajax({ - type: 'GET', - url: helper.scriptLocation() + '?' + helper.pasteId(), - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) - { - controller.displayMessages(data); - } - else if (data.status === 1) - { - controller.showError(i18n._('Could not refresh display: %s', data.message)); - } - else - { - controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); - } - } - }) - .fail(function() { - controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); - }); - } - else if (data.status === 1) + // no "clone" for IE<10. + if ($('#oldienotice').is(":visible")) { - controller.showError(i18n._('Could not post comment: %s', data.message)); + $cloneButton.addClass('hidden'); } else { - controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); + $cloneButton.removeClass('hidden'); } + + $rawTextButton.removeClass('hidden'); + $sendButton.addClass('hidden'); + $attach.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $newButton.removeClass('hidden'); + $preview.addClass('hidden'); } - }) - .fail(function() { - controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); - }); - }; - /** - * send a new paste to server - * - * @name controller.sendData - * @function - * @param {Event} event - */ - me.sendData = function(event) - { - event.preventDefault(); - var file = document.getElementById('file'), - files = (file && file.files) ? file.files : null; // FileList object - - // do not send if no data. - if ($message.val().length === 0 && !(files && files[0])) - { - return; - } + $pasteResult.addClass('hidden'); + $message.addClass('hidden'); + $clearText.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $loadingIndicator.addClass('hidden'); + }; - // if sjcl has not collected enough entropy yet, display a message - if (!sjcl.random.isReady()) + /** + * when "burn after reading" is checked, disable discussion + * + * @name controller.changeBurnAfterReading + * @function + */ + me.changeBurnAfterReading = function() { - me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); - sjcl.random.addEventListener('seeded', function() { - me.sendData(event); - }); - return; - } - - $('.navbar-toggle').click(); - $password.addClass('hidden'); - me.showStatus(i18n._('Sending paste...'), true); - - me.stateSubmittingPaste(); + if ($burnAfterReading.is(':checked') ) + { + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); + } + else + { + $openDisc.removeClass('buttondisabled'); + $openDiscussion.removeAttr('disabled'); + } + }; - var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), - password = $passwordInput.val(); - if(files && files[0]) + /** + * when discussion is checked, disable "burn after reading" + * + * @name controller.changeOpenDisc + * @function + */ + me.changeOpenDisc = function() { - if(typeof FileReader === undefined) + if ($openDiscussion.is(':checked') ) { - // revert loading status… - me.stateNewPaste(); - me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); - return; + $burnAfterReadingOption.addClass('buttondisabled'); + $burnAfterReading.attr({checked: false, disabled: true}); } - var reader = new FileReader(); - // closure to capture the file information - reader.onload = (function(theFile) + else { - return function(e) { - controller.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, e.target.result), - filter.cipher(randomkey, password, theFile.name) - ); - }; - })(files[0]); - reader.readAsDataURL(files[0]); - } - else if($attachmentLink.attr('href')) - { - me.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, $attachmentLink.attr('href')), - $attachmentLink.attr('download') - ); - } - else - { - me.sendDataContinue(randomkey, '', ''); - } - }; - - /** - * send a new paste to server, step 2 - * - * @name controller.sendDataContinue - * @function - * @param {string} randomkey - * @param {string} cipherdata_attachment - * @param {string} cipherdata_attachment_name - */ - me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) - { - var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), - data_to_send = { - data: cipherdata, - expire: $('#pasteExpiration').val(), - formatter: $('#pasteFormatter').val(), - burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, - opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 - }; - if (cipherdata_attachment.length > 0) - { - data_to_send.attachment = cipherdata_attachment; - if (cipherdata_attachment_name.length > 0) - { - data_to_send.attachmentname = cipherdata_attachment_name; - } - } - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) { - me.stateExistingPaste(); - var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, - deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - me.showStatus(''); - $errorMessage.addClass('hidden'); - // show new URL in browser bar - history.pushState({type: 'newpaste'}, document.title, url); - - $('#pastelink').html( - i18n._( - 'Your paste is %s (Hit [Ctrl]+[c] to copy)', - url, url - ) + me.shortenUrl(url) - ); - // save newly created element - $pasteUrl = $('#pasteurl'); - // and add click event - $pasteUrl.click(me.pasteLinkClick); - - var shortenButton = $('#shortenbutton'); - if (shortenButton) { - shortenButton.click(me.sendToShortener); - } - $('#deletelink').html('' + i18n._('Delete data') + ''); - $pasteResult.removeClass('hidden'); - // we pre-select the link so that the user only has to [Ctrl]+[c] the link - helper.selectText($pasteUrl[0]); - me.showStatus(''); - me.formatPaste(data_to_send.formatter, $message.val()); - } - else if (data.status === 1) - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', data.message)); - } - else - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); - } + $burnAfterReadingOption.removeClass('buttondisabled'); + $burnAfterReading.removeAttr('disabled'); } - }) - .fail(function() + }; + + /** + * forward to URL shortener + * + * @name controller.sendToShortener + * @function + * @param {Event} event + */ + me.sendToShortener = function(event) { - // revert loading status… - me.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); - }); - }; + window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl); + event.preventDefault(); + }; - /** - * check if a URL shortener was defined and create HTML containing a link to it - * - * @name controller.shortenUrl - * @function - * @param {string} url - * @return {string} html - */ - me.shortenUrl = function(url) - { - var shortenerHtml = $('#shortenbutton'); - if (shortenerHtml) { - shortenerUrl = shortenerHtml.data('shortener'); - createdPasteUrl = url; - return ' ' + $('
').append(shortenerHtml.clone()).html(); - } - return ''; - }; + /** + * reload the page + * + * This takes the user to the PrivateBin home page. + * + * @name controller.reloadPage + * @function + * @param {Event} event + */ + me.reloadPage = function(event) + { + window.location.href = helper.scriptLocation(); + event.preventDefault(); + }; - /** - * put the screen in "New paste" mode - * - * @name controller.stateNewPaste - * @function - */ - me.stateNewPaste = function() - { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - $sendButton.removeClass('hidden'); - $expiration.removeClass('hidden'); - $formatter.removeClass('hidden'); - $burnAfterReadingOption.removeClass('hidden'); - $openDisc.removeClass('hidden'); - $newButton.removeClass('hidden'); - $password.removeClass('hidden'); - $attach.removeClass('hidden'); - $message.removeClass('hidden'); - $preview.removeClass('hidden'); - $message.focus(); - }; + /** + * return raw text + * + * @name controller.rawText + * @function + * @param {Event} event + */ + me.rawText = function(event) + { + var paste = $('#pasteFormatter').val() === 'markdown' ? + $prettyPrint.text() : $clearText.text(); + history.pushState( + null, document.title, helper.scriptLocation() + '?' + + helper.pasteId() + '#' + helper.pageKey() + ); + // we use text/html instead of text/plain to avoid a bug when + // reloading the raw text view (it reverts to type text/html) + var newDoc = document.open('text/html', 'replace'); + newDoc.write('
' + helper.htmlEntities(paste) + '
'); + newDoc.close(); - /** - * put the screen in mode after submitting a paste - * - * @name controller.stateSubmittingPaste - * @function - */ - me.stateSubmittingPaste = function() - { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $sendButton.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $newButton.addClass('hidden'); - $password.addClass('hidden'); - $attach.addClass('hidden'); - $message.addClass('hidden'); - $preview.addClass('hidden'); - - $loadingIndicator.removeClass('hidden'); - }; + event.preventDefault(); + }; - /** - * put the screen in a state where the only option is to submit a - * new paste - * - * @name controller.stateOnlyNewPaste - * @function - */ - me.stateOnlyNewPaste = function() - { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $sendButton.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $password.addClass('hidden'); - $attach.addClass('hidden'); - $message.addClass('hidden'); - $preview.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - - $newButton.removeClass('hidden'); - }; + /** + * clone the current paste + * + * @name controller.clonePaste + * @function + * @param {Event} event + */ + me.clonePaste = function(event) + { + event.preventDefault(); + me.stateNewPaste(); - /** - * put the screen in "Existing paste" mode - * - * @name controller.stateExistingPaste - * @function - * @param {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false - */ - me.stateExistingPaste = function(preview) - { - preview = preview || false; + // erase the id and the key in url + history.replaceState(null, document.title, helper.scriptLocation()); - if (!preview) - { - // no "clone" for IE<10. - if ($('#oldienotice').is(":visible")) + me.showStatus(''); + if ($attachmentLink.attr('href')) { - $cloneButton.addClass('hidden'); + $clonedFile.removeClass('hidden'); + $fileWrap.addClass('hidden'); } - else - { - $cloneButton.removeClass('hidden'); - } - - $rawTextButton.removeClass('hidden'); - $sendButton.addClass('hidden'); - $attach.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $newButton.removeClass('hidden'); - $preview.addClass('hidden'); - } - - $pasteResult.addClass('hidden'); - $message.addClass('hidden'); - $clearText.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - }; + $message.text( + $('#pasteFormatter').val() === 'markdown' ? + $prettyPrint.text() : $clearText.text() + ); + $('.navbar-toggle').click(); + }; - /** - * when "burn after reading" is checked, disable discussion - * - * @name controller.changeBurnAfterReading - * @function - */ - me.changeBurnAfterReading = function() - { - if ($burnAfterReading.is(':checked') ) + /** + * set the expiration on bootstrap templates + * + * @name controller.setExpiration + * @function + * @param {Event} event + */ + me.setExpiration = function(event) { - $openDisc.addClass('buttondisabled'); - $openDiscussion.attr({checked: false, disabled: true}); - } - else - { - $openDisc.removeClass('buttondisabled'); - $openDiscussion.removeAttr('disabled'); - } - }; + event.preventDefault(); + var target = $(event.target); + $('#pasteExpiration').val(target.data('expiration')); + $('#pasteExpirationDisplay').text(target.text()); + }; - /** - * when discussion is checked, disable "burn after reading" - * - * @name controller.changeOpenDisc - * @function - */ - me.changeOpenDisc = function() - { - if ($openDiscussion.is(':checked') ) + /** + * set the format on bootstrap templates + * + * @name controller.setFormat + * @function + * @param {Event} event + */ + me.setFormat = function(event) { - $burnAfterReadingOption.addClass('buttondisabled'); - $burnAfterReading.attr({checked: false, disabled: true}); - } - else - { - $burnAfterReadingOption.removeClass('buttondisabled'); - $burnAfterReading.removeAttr('disabled'); - } - }; - - /** - * forward to URL shortener - * - * @name controller.sendToShortener - * @function - * @param {Event} event - */ - me.sendToShortener = function(event) - { - window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl); - event.preventDefault(); - }; - - /** - * reload the page - * - * This takes the user to the PrivateBin home page. - * - * @name controller.reloadPage - * @function - * @param {Event} event - */ - me.reloadPage = function(event) - { - window.location.href = helper.scriptLocation(); - event.preventDefault(); - }; + var target = $(event.target); + $('#pasteFormatter').val(target.data('format')); + $('#pasteFormatterDisplay').text(target.text()); - /** - * return raw text - * - * @name controller.rawText - * @function - * @param {Event} event - */ - me.rawText = function(event) - { - var paste = $('#pasteFormatter').val() === 'markdown' ? - $prettyPrint.text() : $clearText.text(); - history.pushState( - null, document.title, helper.scriptLocation() + '?' + - helper.pasteId() + '#' + helper.pageKey() - ); - // we use text/html instead of text/plain to avoid a bug when - // reloading the raw text view (it reverts to type text/html) - var newDoc = document.open('text/html', 'replace'); - newDoc.write('
' + helper.htmlEntities(paste) + '
'); - newDoc.close(); - - event.preventDefault(); - }; - - /** - * clone the current paste - * - * @name controller.clonePaste - * @function - * @param {Event} event - */ - me.clonePaste = function(event) - { - event.preventDefault(); - me.stateNewPaste(); - - // erase the id and the key in url - history.replaceState(null, document.title, helper.scriptLocation()); + if ($messagePreview.parent().hasClass('active')) { + me.viewPreview(event); + } + event.preventDefault(); + }; - me.showStatus(''); - if ($attachmentLink.attr('href')) + /** + * set the language in a cookie and reload the page + * + * @name controller.setLanguage + * @function + * @param {Event} event + */ + me.setLanguage = function(event) { - $clonedFile.removeClass('hidden'); - $fileWrap.addClass('hidden'); - } - $message.text( - $('#pasteFormatter').val() === 'markdown' ? - $prettyPrint.text() : $clearText.text() - ); - $('.navbar-toggle').click(); - }; - - /** - * set the expiration on bootstrap templates - * - * @name controller.setExpiration - * @function - * @param {Event} event - */ - me.setExpiration = function(event) - { - event.preventDefault(); - var target = $(event.target); - $('#pasteExpiration').val(target.data('expiration')); - $('#pasteExpirationDisplay').text(target.text()); - }; - - /** - * set the format on bootstrap templates - * - * @name controller.setFormat - * @function - * @param {Event} event - */ - me.setFormat = function(event) - { - var target = $(event.target); - $('#pasteFormatter').val(target.data('format')); - $('#pasteFormatterDisplay').text(target.text()); - - if ($messagePreview.parent().hasClass('active')) { - me.viewPreview(event); - } - event.preventDefault(); - }; + document.cookie = 'lang=' + $(event.target).data('lang'); + me.reloadPage(event); + }; - /** - * set the language in a cookie and reload the page - * - * @name controller.setLanguage - * @function - * @param {Event} event - */ - me.setLanguage = function(event) - { - document.cookie = 'lang=' + $(event.target).data('lang'); - me.reloadPage(event); - }; + /** + * support input of tab character + * + * @name controller.supportTabs + * @function + * @param {Event} event + * @TODO doc what is @this here? + */ + me.supportTabs = function(event) + { + var keyCode = event.keyCode || event.which; + // tab was pressed + if (keyCode === 9) + { + // prevent the textarea to lose focus + event.preventDefault(); + // get caret position & selection + var val = this.value, + start = this.selectionStart, + end = this.selectionEnd; + // set textarea value to: text before caret + tab + text after caret + this.value = val.substring(0, start) + '\t' + val.substring(end); + // put caret at right position again + this.selectionStart = this.selectionEnd = start + 1; + } + }; - /** - * support input of tab character - * - * @name controller.supportTabs - * @function - * @param {Event} event - * @TODO doc what is @this here? - */ - me.supportTabs = function(event) - { - var keyCode = event.keyCode || event.which; - // tab was pressed - if (keyCode === 9) + /** + * view the editor tab + * + * @name controller.viewEditor + * @function + * @param {Event} event + */ + me.viewEditor = function(event) { - // prevent the textarea to lose focus - event.preventDefault(); - // get caret position & selection - var val = this.value, - start = this.selectionStart, - end = this.selectionEnd; - // set textarea value to: text before caret + tab + text after caret - this.value = val.substring(0, start) + '\t' + val.substring(end); - // put caret at right position again - this.selectionStart = this.selectionEnd = start + 1; - } - }; + $messagePreview.parent().removeClass('active'); + $messageEdit.parent().addClass('active'); + $message.focus(); + me.stateNewPaste(); - /** - * view the editor tab - * - * @name controller.viewEditor - * @function - * @param {Event} event - */ - me.viewEditor = function(event) - { - $messagePreview.parent().removeClass('active'); - $messageEdit.parent().addClass('active'); - $message.focus(); - me.stateNewPaste(); - - event.preventDefault(); - }; + event.preventDefault(); + }; - /** - * view the preview tab - * - * @name controller.viewPreview - * @function - * @param {Event} event - */ - me.viewPreview = function(event) - { - $messageEdit.parent().removeClass('active'); - $messagePreview.parent().addClass('active'); - $message.focus(); - me.stateExistingPaste(true); - me.formatPaste($('#pasteFormatter').val(), $message.val()); - - event.preventDefault(); - }; + /** + * view the preview tab + * + * @name controller.viewPreview + * @function + * @param {Event} event + */ + me.viewPreview = function(event) + { + $messageEdit.parent().removeClass('active'); + $messagePreview.parent().addClass('active'); + $message.focus(); + me.stateExistingPaste(true); + me.formatPaste($('#pasteFormatter').val(), $message.val()); - /** - * handle history (pop) state changes - * - * currently this does only handle redirects to the home page. - * - * @name controller.historyChange - * @function - * @param {Event} event - */ - me.historyChange = function(event) - { - var currentLocation = helper.scriptLocation(); - if (event.originalEvent.state === null && // no state object passed - event.originalEvent.target.location.href === currentLocation && // target location is home page - window.location.href === currentLocation // and we are not already on the home page - ) { - // redirect to home page - window.location.href = currentLocation; - } - }; + event.preventDefault(); + }; - /** - * Forces opening the paste if the link does not do this automatically. - * - * This is necessary as browsers will not reload the page when it is - * already loaded (which is fake as it is set via history.pushState()). - * - * @name controller.pasteLinkClick - * @function - * @param {Event} event - */ - me.pasteLinkClick = function(event) - { - // check if location is (already) shown in URL bar - if (window.location.href === $pasteUrl.attr('href')) { - // if so we need to load link by reloading the current site - window.location.reload(true); - } - }; + /** + * handle history (pop) state changes + * + * currently this does only handle redirects to the home page. + * + * @name controller.historyChange + * @function + * @param {Event} event + */ + me.historyChange = function(event) + { + var currentLocation = helper.scriptLocation(); + if (event.originalEvent.state === null && // no state object passed + event.originalEvent.target.location.href === currentLocation && // target location is home page + window.location.href === currentLocation // and we are not already on the home page + ) { + // redirect to home page + window.location.href = currentLocation; + } + }; - /** - * create a new paste - * - * @name controller.newPaste - * @function - */ - me.newPaste = function() - { - me.stateNewPaste(); - me.showStatus(''); - $message.text(''); - me.changeBurnAfterReading(); - me.changeOpenDisc(); - }; + /** + * Forces opening the paste if the link does not do this automatically. + * + * This is necessary as browsers will not reload the page when it is + * already loaded (which is fake as it is set via history.pushState()). + * + * @name controller.pasteLinkClick + * @function + * @param {Event} event + */ + me.pasteLinkClick = function(event) + { + // check if location is (already) shown in URL bar + if (window.location.href === $pasteUrl.attr('href')) { + // if so we need to load link by reloading the current site + window.location.reload(true); + } + }; - /** - * removes an attachment - * - * @name controller.removeAttachment - * @function - */ - me.removeAttachment = function() - { - $clonedFile.addClass('hidden'); - // removes the saved decrypted file data - $attachmentLink.attr('href', ''); - // the only way to deselect the file is to recreate the input // @TODO really? - $fileWrap.html($fileWrap.html()); - $fileWrap.removeClass('hidden'); - }; + /** + * create a new paste + * + * @name controller.newPaste + * @function + */ + me.newPaste = function() + { + me.stateNewPaste(); + me.showStatus(''); + $message.text(''); + me.changeBurnAfterReading(); + me.changeOpenDisc(); + }; - /** - * decrypt using the password from the modal dialog - * - * @name controller.decryptPasswordModal - * @function - */ - me.decryptPasswordModal = function() - { - $passwordInput.val($passwordDecrypt.val()); - me.displayMessages(); - }; + /** + * removes an attachment + * + * @name controller.removeAttachment + * @function + */ + me.removeAttachment = function() + { + $clonedFile.addClass('hidden'); + // removes the saved decrypted file data + $attachmentLink.attr('href', ''); + // the only way to deselect the file is to recreate the input // @TODO really? + $fileWrap.html($fileWrap.html()); + $fileWrap.removeClass('hidden'); + }; - /** - * submit a password in the modal dialog - * - * @name controller.submitPasswordModal - * @function - * @param {Event} event - */ - me.submitPasswordModal = function(event) - { - event.preventDefault(); - $passwordModal.modal('hide'); - }; + /** + * decrypt using the password from the modal dialog + * + * @name controller.decryptPasswordModal + * @function + */ + me.decryptPasswordModal = function() + { + $passwordInput.val($passwordDecrypt.val()); + me.displayMessages(); + }; - /** - * display an error message, - * we use the same function for paste and reply to comments - * - * @name controller.showError - * @function - * @param {string} message - text to display - */ - me.showError = function(message) - { - if ($status.length) + /** + * submit a password in the modal dialog + * + * @name controller.submitPasswordModal + * @function + * @param {Event} event + */ + me.submitPasswordModal = function(event) { - $status.addClass('errorMessage').text(message); - } - else + event.preventDefault(); + $passwordModal.modal('hide'); + }; + + /** + * display an error message, + * we use the same function for paste and reply to comments + * + * @name controller.showError + * @function + * @param {string} message - text to display + */ + me.showError = function(message) { - $errorMessage.removeClass('hidden'); - helper.setMessage($errorMessage, message); - } - if (typeof $replyStatus !== 'undefined') { - $replyStatus.addClass('errorMessage'); - $replyStatus.addClass($errorMessage.attr('class')); if ($status.length) { - $replyStatus.html($status.html()); + $status.addClass('errorMessage').text(message); } else { - $replyStatus.html($errorMessage.html()); + $errorMessage.removeClass('hidden'); + helper.setMessage($errorMessage, message); } - } - }; + if (typeof $replyStatus !== 'undefined') { + $replyStatus.addClass('errorMessage'); + $replyStatus.addClass($errorMessage.attr('class')); + if ($status.length) + { + $replyStatus.html($status.html()); + } + else + { + $replyStatus.html($errorMessage.html()); + } + } + }; - /** - * display a status message, - * we use the same function for paste and reply to comments - * - * @name controller.showStatus - * @function - * @param {string} message - text to display - * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false - */ - me.showStatus = function(message, spin) - { - if (spin || false) + /** + * display a status message, + * we use the same function for paste and reply to comments + * + * @name controller.showStatus + * @function + * @param {string} message - text to display + * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false + */ + me.showStatus = function(message, spin) { - var img = ''; - $status.prepend(img); + if (spin || false) + { + var img = ''; + $status.prepend(img); + if (typeof $replyStatus !== 'undefined') { + $replyStatus.prepend(img); + } + } if (typeof $replyStatus !== 'undefined') { - $replyStatus.prepend(img); + $replyStatus.removeClass('errorMessage').text(message); } - } - if (typeof $replyStatus !== 'undefined') { - $replyStatus.removeClass('errorMessage').text(message); - } - if (!message) - { - $status.html(' '); - return; - } - if (message === '') - { - $status.html(' '); - return; - } - $status.removeClass('errorMessage').text(message); - }; - - /** - * bind events to DOM elements - * - * @private - * @function - */ - function bindEvents() - { - $burnAfterReading.change(me.changeBurnAfterReading); - $openDisc.change(me.changeOpenDisc); - $sendButton.click(me.sendData); - $cloneButton.click(me.clonePaste); - $rawTextButton.click(me.rawText); - $fileRemoveButton.click(me.removeAttachment); - $('.reloadlink').click(me.reloadPage); - $message.keydown(me.supportTabs); - $messageEdit.click(me.viewEditor); - $messagePreview.click(me.viewPreview); - - // bootstrap template drop downs - $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration); - $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat); - $('#language ul.dropdown-menu li a').click(me.setLanguage); - - // page template drop down - $('#language select option').click(me.setLanguage); - - // focus password input when it is shown - $passwordModal.on('shown.bs.modal', function () { - $passwordDecrypt.focus(); - }); - // handle modal password request on decryption - $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal); - $passwordForm.submit(me.submitPasswordModal); - - $(window).on('popstate', me.historyChange); - } + if (!message) + { + $status.html(' '); + return; + } + if (message === '') + { + $status.html(' '); + return; + } + $status.removeClass('errorMessage').text(message); + }; - /** - * main application - * - * @name controller.init - * @function - */ - me.init = function() - { - // hide "no javascript" message - $('#noscript').hide(); - - // preload jQuery wrapped DOM elements and bind events - $attach = $('#attach'); - $attachment = $('#attachment'); - $attachmentLink = $('#attachment a'); - $burnAfterReading = $('#burnafterreading'); - $burnAfterReadingOption = $('#burnafterreadingoption'); - $cipherData = $('#cipherdata'); - $clearText = $('#cleartext'); - $cloneButton = $('#clonebutton'); - $clonedFile = $('#clonedfile'); - $comments = $('#comments'); - $discussion = $('#discussion'); - $errorMessage = $('#errormessage'); - $expiration = $('#expiration'); - $fileRemoveButton = $('#fileremovebutton'); - $fileWrap = $('#filewrap'); - $formatter = $('#formatter'); - $image = $('#image'); - $loadingIndicator = $('#loadingindicator'); - $message = $('#message'); - $messageEdit = $('#messageedit'); - $messagePreview = $('#messagepreview'); - $newButton = $('#newbutton'); - $openDisc = $('#opendisc'); - $openDiscussion = $('#opendiscussion'); - $password = $('#password'); - $passwordInput = $('#passwordinput'); - $passwordModal = $('#passwordmodal'); - $passwordForm = $('#passwordform'); - $passwordDecrypt = $('#passworddecrypt'); - $pasteResult = $('#pasteresult'); - // $pasteUrl is saved in sendDataContinue() if/after it is - // actually created - $prettyMessage = $('#prettymessage'); - $prettyPrint = $('#prettyprint'); - $preview = $('#preview'); - $rawTextButton = $('#rawtextbutton'); - $remainingTime = $('#remainingtime'); - // $replyStatus is saved in openReply() - $sendButton = $('#sendbutton'); - $status = $('#status'); - bindEvents(); - - // display status returned by php code, if any (eg. paste was properly deleted) - if ($status.text().length > 0) + /** + * bind events to DOM elements + * + * @private + * @function + */ + function bindEvents() { - me.showStatus($status.text()); - return; - } + $burnAfterReading.change(me.changeBurnAfterReading); + $openDisc.change(me.changeOpenDisc); + $sendButton.click(me.sendData); + $cloneButton.click(me.clonePaste); + $rawTextButton.click(me.rawText); + $fileRemoveButton.click(me.removeAttachment); + $('.reloadlink').click(me.reloadPage); + $message.keydown(me.supportTabs); + $messageEdit.click(me.viewEditor); + $messagePreview.click(me.viewPreview); + + // bootstrap template drop downs + $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration); + $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat); + $('#language ul.dropdown-menu li a').click(me.setLanguage); + + // page template drop down + $('#language select option').click(me.setLanguage); + + // focus password input when it is shown + $passwordModal.on('shown.bs.modal', function () { + $passwordDecrypt.focus(); + }); + // handle modal password request on decryption + $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal); + $passwordForm.submit(me.submitPasswordModal); - // keep line height even if content empty - $status.html(' '); + $(window).on('popstate', me.historyChange); + }; - // display an existing paste - if ($cipherData.text().length > 1) + /** + * main application + * + * @name controller.init + * @function + */ + me.init = function() { - // missing decryption key in URL? - if (window.location.hash.length === 0) + // hide "no javascript" message + $('#noscript').hide(); + + // preload jQuery wrapped DOM elements and bind events + $attach = $('#attach'); + $attachment = $('#attachment'); + $attachmentLink = $('#attachment a'); + $burnAfterReading = $('#burnafterreading'); + $burnAfterReadingOption = $('#burnafterreadingoption'); + $cipherData = $('#cipherdata'); + $clearText = $('#cleartext'); + $cloneButton = $('#clonebutton'); + $clonedFile = $('#clonedfile'); + $comments = $('#comments'); + $discussion = $('#discussion'); + $errorMessage = $('#errormessage'); + $expiration = $('#expiration'); + $fileRemoveButton = $('#fileremovebutton'); + $fileWrap = $('#filewrap'); + $formatter = $('#formatter'); + $image = $('#image'); + $loadingIndicator = $('#loadingindicator'); + $message = $('#message'); + $messageEdit = $('#messageedit'); + $messagePreview = $('#messagepreview'); + $newButton = $('#newbutton'); + $openDisc = $('#opendisc'); + $openDiscussion = $('#opendiscussion'); + $password = $('#password'); + $passwordInput = $('#passwordinput'); + $passwordModal = $('#passwordmodal'); + $passwordForm = $('#passwordform'); + $passwordDecrypt = $('#passworddecrypt'); + $pasteResult = $('#pasteresult'); + // $pasteUrl is saved in sendDataContinue() if/after it is + // actually created + $prettyMessage = $('#prettymessage'); + $prettyPrint = $('#prettyprint'); + $preview = $('#preview'); + $rawTextButton = $('#rawtextbutton'); + $remainingTime = $('#remainingtime'); + // $replyStatus is saved in openReply() + $sendButton = $('#sendbutton'); + $status = $('#status'); + bindEvents(); + + // display status returned by php code, if any (eg. paste was properly deleted) + if ($status.text().length > 0) { - me.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)')); + me.showStatus($status.text()); return; } - // show proper elements on screen - me.stateExistingPaste(); - me.displayMessages(); - } - // display error message from php code - else if ($errorMessage.text().length > 1) - { - me.showError($errorMessage.text()); - } - // create a new paste - else - { - me.newPaste(); - } - }; + // keep line height even if content empty + $status.html(' '); - return me; -})(window, document, jQuery, sjcl, Base64, RawDeflate); + // display an existing paste + if ($cipherData.text().length > 1) + { + // missing decryption key in URL? + if (window.location.hash.length === 0) + { + me.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)')); + return; + } + + // show proper elements on screen + me.stateExistingPaste(); + me.displayMessages(); + } + // display error message from php code + else if ($errorMessage.text().length > 1) + { + me.showError($errorMessage.text()); + } + // create a new paste + else + { + me.newPaste(); + } + }; + + return me; + })(window, document); + + /** + * main application start, called when DOM is fully loaded and + * runs controller initalization after translations are loaded + */ + $(i18n.loadTranslations); + + return { + helper: helper, + i18n: i18n, + filter: filter, + controller: controller + }; +}(jQuery, sjcl, Base64, RawDeflate); From dd721c651b216fd334d985a5f282a079f37ab86d Mon Sep 17 00:00:00 2001 From: rugk Date: Sat, 11 Feb 2017 16:19:59 +0100 Subject: [PATCH 06/59] Update SRI hashes Fixes https://github.com/PrivateBin/PrivateBin/issues/181 --- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 60c6727..4bf3ca1 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + diff --git a/tpl/page.php b/tpl/page.php index c92136f..4ae0b6a 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From dd6e426da79f00f3554df7e7700777ea3ee38e3d Mon Sep 17 00:00:00 2001 From: rugk Date: Sun, 12 Feb 2017 18:08:08 +0100 Subject: [PATCH 07/59] first round of refactoring split into modules, moved code around need to make it work --- js/privatebin.js | 1722 +++++++++++++++++++++++++-------------------- tpl/bootstrap.php | 23 +- 2 files changed, 988 insertions(+), 757 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 6322c0e..0416a03 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -11,7 +11,6 @@ * @namespace */ -'use strict'; /** global: Base64 */ /** global: FileReader */ /** global: RawDeflate */ @@ -25,17 +24,14 @@ // Immediately start random number generator collector. sjcl.random.startCollectors(); -// jQuery(document).ready(function() { -// // startup -// } - jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { + 'use strict'; + /** * static helper methods * * @param {object} window * @param {object} document - * @name helper * @class */ var helper = (function (window, document) { @@ -157,27 +153,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } }; - /** - * replace last child of element with message - * - * @name helper.setMessage - * @function - * @param {jQuery} $element - a jQuery wrapped DOM element - * @param {string} message - the message to append - */ - me.setMessage = function($element, message) - { - var content = $element.contents(); - if (content.length > 0) - { - content[content.length - 1].nodeValue = ' ' + message; - } - else - { - me.setElementText($element, message); - } - }; - /** * convert URLs to clickable links. * URLs to handle: @@ -367,7 +342,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @param {object} window * @param {object} document - * @name i18n * @class */ var i18n = (function (window, document) { @@ -516,17 +490,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } // if language is already used (e.g, default 'en'), skip update - if (newLanguage === language) - { - controller.init(); + if (newLanguage === language) { return; } // if language is not supported, show error - if (supportedLanguages.indexOf(newLanguage) === -1) - { + if (supportedLanguages.indexOf(newLanguage) === -1) { console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage); - controller.init(); } // load strongs from JSON @@ -536,54 +506,53 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }).fail(function (data, textStatus, errorMsg) { console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); }); - - controller.init(); }; return me; })(window, document); /** - * filter methods + * cryptTool methods * * @param {object} window * @param {object} document - * @name filter * @class */ - var filter = (function (window, document) { + var cryptTool = (function () { var me = {}; /** * compress a message (deflate compression), returns base64 encoded data * - * @name filter.compress + * @name cryptToolcompress * @function + * @private * @param {string} message * @return {string} base64 data */ - me.compress = function(message) + function compress(message) { return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) ); - }, + } /** - * decompress a message compressed with filter.compress() + * decompress a message compressed with cryptToolcompress() * - * @name filter.decompress + * @name cryptTooldecompress * @function + * @private * @param {string} data - base64 data * @return {string} message */ - me.decompress = function(data) + function decompress(data) { return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) ); - }, + } /** * compress, then encrypt message with given key and password * - * @name filter.cipher + * @name cryptToolcipher * @function * @param {string} key * @param {string} password @@ -596,15 +565,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var options = {mode: 'gcm', ks: 256, ts: 128}; if ((password || '').trim().length === 0) { - return sjcl.encrypt(key, me.compress(message), options); + return sjcl.encrypt(key, compress(message), options); } return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options); - }, + }; /** * decrypt message with key, then decompress * - * @name filter.decipher + * @name cryptTooldecipher * @function * @param {string} key * @param {string} password @@ -617,13 +586,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { try { - return me.decompress(sjcl.decrypt(key, data)); + return decompress(sjcl.decrypt(key, data)); } catch(err) { try { - return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); + return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); } catch(e) { @@ -632,113 +601,35 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } return ''; - } + }; return me; - })(window, document); + })(); /** - * PrivateBin logic + * User interface manager * * @param {object} window * @param {object} document - * @name controller * @class */ - var controller = (function (window, document) { + var uiMan = (function (window, document) { var me = {}; - /** - * headers to send in AJAX requests - * - * @private - * @enum {Object} - */ - var headers = {'X-Requested-With': 'JSONHttpRequest'}; - - /** - * URL shortners create address - * - * @private - * @prop {string} - */ - var shortenerUrl = ''; - - /** - * URL of newly created paste - * - * @private - * @prop {string} - */ - var createdPasteUrl = ''; - // jQuery pre-loaded objects - var $attach, - $attachment, - $attachmentLink, - $burnAfterReading, - $burnAfterReadingOption, - $cipherData, + var $cipherData, $clearText, - $cloneButton, $clonedFile, $comments, $discussion, - $errorMessage, - $expiration, - $fileRemoveButton, - $fileWrap, - $formatter, $image, - $loadingIndicator, - $message, - $messageEdit, - $messagePreview, - $newButton, - $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear - $openDiscussion, - $password, - $passwordInput, - $passwordModal, - $passwordForm, - $passwordDecrypt, $pasteResult, $pasteUrl, $prettyMessage, $prettyPrint, $preview, - $rawTextButton, $remainingTime, - $replyStatus, - $sendButton, - $status; - - /** - * ask the user for the password and set it - * - * @name controller.requestPassword - * @function - */ - me.requestPassword = function() - { - if ($passwordModal.length === 0) { - var password = prompt(i18n._('Please enter the password for this paste:'), ''); - if (password === null) - { - throw 'password prompt canceled'; - } - if (password.length === 0) - { - // recursive… - me.requestPassword(); - } else { - $passwordInput.val(password); - me.displayMessages(); - } - } else { - $passwordModal.modal(); - } - }; + $replyStatus; /** * use given format on paste, defaults to plain text @@ -827,7 +718,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { if (paste.attachment) { - var attachment = filter.decipher(key, password, paste.attachment); + var attachment = cryptTooldecipher(key, password, paste.attachment); if (attachment.length === 0) { if (password.length === 0) @@ -835,7 +726,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.requestPassword(); return; } - attachment = filter.decipher(key, password, paste.attachment); + attachment = cryptTooldecipher(key, password, paste.attachment); } if (attachment.length === 0) { @@ -844,7 +735,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (paste.attachmentname) { - var attachmentname = filter.decipher(key, password, paste.attachmentname); + var attachmentname = cryptTooldecipher(key, password, paste.attachmentname); if (attachmentname.length > 0) { $attachmentLink.attr('download', attachmentname); @@ -865,7 +756,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $image.removeClass('hidden'); } } - var cleartext = filter.decipher(key, password, paste.data); + var cleartext = cryptTooldecipher(key, password, paste.data); if (cleartext.length === 0 && password.length === 0 && !paste.attachment) { me.requestPassword(); @@ -899,7 +790,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { 'This document will expire in %d ' + expiration[1] + '.', 'This document will expire in %d ' + expiration[1] + 's.' ]; - helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0])); + me.appendMessage($remainingTime, i18n._(expirationLabel, expiration[0])); $remainingTime.removeClass('foryoureyesonly') .removeClass('hidden'); } @@ -916,7 +807,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { .fail(function() { controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }); - helper.setMessage($remainingTime, i18n._( + me.appendMessage($remainingTime, i18n._( 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' )); $remainingTime.addClass('foryoureyesonly') @@ -937,7 +828,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { var $place = $comments, comment = paste.comments[i], - commentText = filter.decipher(key, password, comment.data), + commentText = cryptTooldecipher(key, password, comment.data), $parentComment = $('#comment_' + comment.parentid); $divComment = $('

' + - '
' - ); - reply.find('button').click( + $reply = $('#replytemplate'); + $reply.find('button').click( {parentid: commentid}, me.sendComment ); - source.after(reply); - $replyStatus = $('#replystatus'); + source.after($reply); + $replyStatus = $('#replystatus'); // when ID --> put into HTML $('#replymessage').focus(); }; /** - * send a reply in a discussion + * replace last child of element with message * - * @name controller.sendComment + * @name me.appendMessage * @function - * @param {Event} event + * @param {jQuery} $element - a jQuery wrapped DOM element + * @param {string} message - the message to append + * @TODO: make private if possible */ - me.sendComment = function(event) + me.appendMessage = function($element, message) { - event.preventDefault(); - $errorMessage.addClass('hidden'); - // do not send if no data - var replyMessage = $('#replymessage'); - if (replyMessage.val().length === 0) + var content = $element.contents(); + if (content.length > 0) { - return; + content[content.length - 1].nodeValue = ' ' + message; } - - me.showStatus(i18n._('Sending comment...'), true); - var parentid = event.data.parentid, - key = helper.pageKey(), - cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), - ciphernickname = '', - nick = $('#nickname').val(); - if (nick.length > 0) + else { - ciphernickname = filter.cipher(key, $passwordInput.val(), nick); + me.setElementText($element, message); } - var data_to_send = { - data: cipherdata, - parentid: parentid, - pasteid: helper.pasteId(), - nickname: ciphernickname - }; - - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) - { - controller.showStatus(i18n._('Comment posted.')); - $.ajax({ - type: 'GET', - url: helper.scriptLocation() + '?' + helper.pasteId(), - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) - { - controller.displayMessages(data); - } - else if (data.status === 1) - { - controller.showError(i18n._('Could not refresh display: %s', data.message)); - } - else - { - controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); - } - } - }) - .fail(function() { - controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); - }); - } - else if (data.status === 1) - { - controller.showError(i18n._('Could not post comment: %s', data.message)); - } - else - { - controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); - } - } - }) - .fail(function() { - controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); - }); }; /** - * send a new paste to server + * handle history (pop) state changes * - * @name controller.sendData + * currently this does only handle redirects to the home page. + * + * @name controller.historyChange * @function * @param {Event} event */ - me.sendData = function(event) + me.historyChange = function(event) { - event.preventDefault(); - var file = document.getElementById('file'), - files = (file && file.files) ? file.files : null; // FileList object - - // do not send if no data. - if ($message.val().length === 0 && !(files && files[0])) - { - return; + var currentLocation = helper.scriptLocation(); + if (event.originalEvent.state === null && // no state object passed + event.originalEvent.target.location.href === currentLocation && // target location is home page + window.location.href === currentLocation // and we are not already on the home page + ) { + // redirect to home page + window.location.href = currentLocation; } + }; - // if sjcl has not collected enough entropy yet, display a message - if (!sjcl.random.isReady()) - { - me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); - sjcl.random.addEventListener('seeded', function() { - me.sendData(event); - }); - return; + /** + * Forces opening the paste if the link does not do this automatically. + * + * This is necessary as browsers will not reload the page when it is + * already loaded (which is fake as it is set via history.pushState()). + * + * @name controller.pasteLinkClick + * @function + * @param {Event} event + */ + me.pasteLinkClick = function(event) + { + // check if location is (already) shown in URL bar + if (window.location.href === $pasteUrl.attr('href')) { + // if so we need to load link by reloading the current site + window.location.reload(true); } + }; - $('.navbar-toggle').click(); - $password.addClass('hidden'); - me.showStatus(i18n._('Sending paste...'), true); - - me.stateSubmittingPaste(); - - var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), - password = $passwordInput.val(); - if(files && files[0]) - { - if(typeof FileReader === undefined) - { - // revert loading status… - me.stateNewPaste(); - me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); - return; - } - var reader = new FileReader(); - // closure to capture the file information - reader.onload = (function(theFile) - { - return function(e) { - controller.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, e.target.result), - filter.cipher(randomkey, password, theFile.name) - ); - }; - })(files[0]); - reader.readAsDataURL(files[0]); - } - else if($attachmentLink.attr('href')) - { - me.sendDataContinue( - randomkey, - filter.cipher(randomkey, password, $attachmentLink.attr('href')), - $attachmentLink.attr('download') - ); - } - else - { - me.sendDataContinue(randomkey, '', ''); - } - }; + /** + * reload the page + * + * This takes the user to the PrivateBin home page. + * + * @name controller.reloadPage + * @function + * @param {Event} event + */ + me.reloadPage = function(event) + { + window.location.href = helper.scriptLocation(); + event.preventDefault(); + }; /** - * send a new paste to server, step 2 + * main UI manager * - * @name controller.sendDataContinue + * @name controller.init * @function - * @param {string} randomkey - * @param {string} cipherdata_attachment - * @param {string} cipherdata_attachment_name */ - me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) + me.init = function() { - var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), - data_to_send = { - data: cipherdata, - expire: $('#pasteExpiration').val(), - formatter: $('#pasteFormatter').val(), - burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, - opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 - }; - if (cipherdata_attachment.length > 0) - { - data_to_send.attachment = cipherdata_attachment; - if (cipherdata_attachment_name.length > 0) - { - data_to_send.attachmentname = cipherdata_attachment_name; - } - } - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) - { - if (data.status === 0) { - me.stateExistingPaste(); - var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, - deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - me.showStatus(''); - $errorMessage.addClass('hidden'); - // show new URL in browser bar - history.pushState({type: 'newpaste'}, document.title, url); + // hide "no javascript" message + $('#noscript').hide(); - $('#pastelink').html( - i18n._( - 'Your paste is %s (Hit [Ctrl]+[c] to copy)', - url, url - ) + me.shortenUrl(url) - ); - // save newly created element - $pasteUrl = $('#pasteurl'); - // and add click event - $pasteUrl.click(me.pasteLinkClick); + // preload jQuery elements + $cipherData = $('#cipherdata'); + $clearText = $('#cleartext'); + $clonedFile = $('#clonedfile'); + $comments = $('#comments'); + $discussion = $('#discussion'); + $errorMessage = $('#errormessage'); + $image = $('#image'); + $pasteResult = $('#pasteresult'); + // $pasteUrl is saved in sendDataContinue() if/after it is + // actually created + $prettyMessage = $('#prettymessage'); + $prettyPrint = $('#prettyprint'); + $preview = $('#preview'); + $remainingTime = $('#remainingtime'); - var shortenButton = $('#shortenbutton'); - if (shortenButton) { - shortenButton.click(me.sendToShortener); - } - $('#deletelink').html('' + i18n._('Delete data') + ''); - $pasteResult.removeClass('hidden'); - // we pre-select the link so that the user only has to [Ctrl]+[c] the link - helper.selectText($pasteUrl[0]); - me.showStatus(''); - me.formatPaste(data_to_send.formatter, $message.val()); - } - else if (data.status === 1) - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', data.message)); - } - else - { - // revert loading status… - controller.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); - } - } - }) - .fail(function() - { - // revert loading status… - me.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); - }); - }; + // bind events + $('.reloadlink').click(me.reloadPage); - /** - * check if a URL shortener was defined and create HTML containing a link to it - * - * @name controller.shortenUrl - * @function - * @param {string} url - * @return {string} html - */ - me.shortenUrl = function(url) - { - var shortenerHtml = $('#shortenbutton'); - if (shortenerHtml) { - shortenerUrl = shortenerHtml.data('shortener'); - createdPasteUrl = url; - return ' ' + $('
').append(shortenerHtml.clone()).html(); - } - return ''; + // bootstrap template drop downs + $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration); + $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat); + + $(window).on('popstate', me.historyChange); }; + return me; + })(window, document); + + /** + * UI state manager + * + * @param {object} window + * @param {object} document + * @class + */ + var state = (function (window, document) { + var me = {}; + /** * put the screen in "New paste" mode * @@ -1306,27 +1049,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.stateNewPaste = function() { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); + $remainingTime.removeClass('hidden'); + $loadingIndicator.addClass('hidden'); - $sendButton.removeClass('hidden'); - $expiration.removeClass('hidden'); - $formatter.removeClass('hidden'); - $burnAfterReadingOption.removeClass('hidden'); - $openDisc.removeClass('hidden'); - $newButton.removeClass('hidden'); - $password.removeClass('hidden'); - $attach.removeClass('hidden'); - $message.removeClass('hidden'); - $preview.removeClass('hidden'); - $message.focus(); + console.error('stateNewPaste is depreciated'); }; /** @@ -1337,27 +1063,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.stateSubmittingPaste = function() { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $sendButton.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $newButton.addClass('hidden'); - $password.addClass('hidden'); - $attach.addClass('hidden'); - $message.addClass('hidden'); - $preview.addClass('hidden'); - - $loadingIndicator.removeClass('hidden'); + console.error('stateSubmittingPaste is depreciated'); }; /** @@ -1369,27 +1075,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.stateOnlyNewPaste = function() { - $message.text(''); - $attachment.addClass('hidden'); - $cloneButton.addClass('hidden'); - $rawTextButton.addClass('hidden'); - $remainingTime.addClass('hidden'); - $pasteResult.addClass('hidden'); - $clearText.addClass('hidden'); - $discussion.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $sendButton.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $password.addClass('hidden'); - $attach.addClass('hidden'); - $message.addClass('hidden'); - $preview.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - - $newButton.removeClass('hidden'); + console.error('stateOnlyNewPaste is depreciated'); }; /** @@ -1402,6 +1088,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.stateExistingPaste = function(preview) { preview = preview || false; + console.error('stateExistingPaste is depreciated'); if (!preview) { @@ -1415,200 +1102,255 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $cloneButton.removeClass('hidden'); } - $rawTextButton.removeClass('hidden'); - $sendButton.addClass('hidden'); - $attach.addClass('hidden'); - $expiration.addClass('hidden'); - $formatter.addClass('hidden'); - $burnAfterReadingOption.addClass('hidden'); - $openDisc.addClass('hidden'); - $newButton.removeClass('hidden'); - $preview.addClass('hidden'); + console.log('show no preview'); } - - $pasteResult.addClass('hidden'); - $message.addClass('hidden'); - $clearText.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $loadingIndicator.addClass('hidden'); }; + return me; + })(window, document); + + /** + * UI status/error manager + * + * @param {object} window + * @param {object} document + * @class + */ + var status = (function (window, document) { + var me = {}; + + var $errorMessage, + $status, + $loadingIndicator; + /** - * when "burn after reading" is checked, disable discussion + * display a status message * - * @name controller.changeBurnAfterReading + * @name controller.showStatus * @function + * @param {string} message - text to display + * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false */ - me.changeBurnAfterReading = function() + me.showStatus = function(message, spin) { - if ($burnAfterReading.is(':checked') ) - { - $openDisc.addClass('buttondisabled'); - $openDiscussion.attr({checked: false, disabled: true}); - } - else - { - $openDisc.removeClass('buttondisabled'); - $openDiscussion.removeAttr('disabled'); - } + // spin is ignored for now + $status.text(message); }; + // @TODO: add showLoading() + /** - * when discussion is checked, disable "burn after reading" + * display a status message for replying to comments * - * @name controller.changeOpenDisc + * @name controller.showStatus * @function + * @param {string} message - text to display + * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false */ - me.changeOpenDisc = function() + me.showReplyStatus = function(message, spin) { - if ($openDiscussion.is(':checked') ) - { - $burnAfterReadingOption.addClass('buttondisabled'); - $burnAfterReading.attr({checked: false, disabled: true}); - } - else - { - $burnAfterReadingOption.removeClass('buttondisabled'); - $burnAfterReading.removeAttr('disabled'); + if (spin || false) { + $replyStatus.find('.spinner').removeClass('hidden') } + $replyStatus.text(message); }; /** - * forward to URL shortener + * hides any status messages * - * @name controller.sendToShortener + * @name controller.hideSTatus * @function - * @param {Event} event */ - me.sendToShortener = function(event) + me.hideStatus = function() { - window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl); - event.preventDefault(); + $status.html(' '); }; /** - * reload the page - * - * This takes the user to the PrivateBin home page. + * display an error message * - * @name controller.reloadPage + * @name controller.showError * @function - * @param {Event} event + * @param {string} message - text to display */ - me.reloadPage = function(event) + me.showError = function(message) { - window.location.href = helper.scriptLocation(); - event.preventDefault(); + $errorMessage.removeClass('hidden'); + me.appendMessage($errorMessage, message); }; /** - * return raw text + * display an error message * - * @name controller.rawText + * @name controller.showError * @function - * @param {Event} event + * @param {string} message - text to display */ - me.rawText = function(event) + me.showReplyError = function(message) { - var paste = $('#pasteFormatter').val() === 'markdown' ? - $prettyPrint.text() : $clearText.text(); - history.pushState( - null, document.title, helper.scriptLocation() + '?' + - helper.pasteId() + '#' + helper.pageKey() - ); - // we use text/html instead of text/plain to avoid a bug when - // reloading the raw text view (it reverts to type text/html) - var newDoc = document.open('text/html', 'replace'); - newDoc.write('
' + helper.htmlEntities(paste) + '
'); - newDoc.close(); + $replyStatus.addClass('alert-danger'); + $replyStatus.addClass($errorMessage.attr('class')); // @TODO ???? - event.preventDefault(); + $replyStatus.text(message); }; /** - * clone the current paste + * init status manager * - * @name controller.clonePaste + * preloads jQuery elements + * + * @name controller.init * @function - * @param {Event} event */ - me.clonePaste = function(event) + me.init = function() { - event.preventDefault(); - me.stateNewPaste(); + // hide "no javascript" message + $('#noscript').hide(); - // erase the id and the key in url - history.replaceState(null, document.title, helper.scriptLocation()); + $loadingIndicator = $('#loadingindicator'); // TODO: integrate $loadingIndicator into this module or leave it in state and remove it here + $errorMessage = $('#errormessage'); + $status = $('#status'); + // @TODO $replyStatus … - me.showStatus(''); - if ($attachmentLink.attr('href')) + // display status returned by php code, if any (eg. paste was properly deleted) + // @TODO remove this by handling errors in a different way + if ($status.text().length > 0) { - $clonedFile.removeClass('hidden'); - $fileWrap.addClass('hidden'); + me.showStatus($status.text()); + return; } - $message.text( - $('#pasteFormatter').val() === 'markdown' ? - $prettyPrint.text() : $clearText.text() - ); - $('.navbar-toggle').click(); + + // keep line height even if content empty + $status.html(' '); // @TODO what? remove? }; + return me; + })(window, document); + + /** + * Passwort modal manager + * + * @param {object} window + * @param {object} document + * @name modal + * @class + */ + var modal = (function (window, document) { + var me = {}; + + var $password, + $passwordInput, + $passwordModal, + $passwordForm, + $passwordDecrypt; + /** - * set the expiration on bootstrap templates + * ask the user for the password and set it * - * @name controller.setExpiration + * @name controller.requestPassword * @function - * @param {Event} event */ - me.setExpiration = function(event) + me.requestPassword = function() { - event.preventDefault(); - var target = $(event.target); - $('#pasteExpiration').val(target.data('expiration')); - $('#pasteExpirationDisplay').text(target.text()); + if ($passwordModal.length === 0) { + var password = prompt(i18n._('Please enter the password for this paste:'), ''); + if (password === null) + { + throw 'password prompt canceled'; + } + if (password.length === 0) + { + // recursive… + me.requestPassword(); + } else { + $passwordInput.val(password); + me.displayMessages(); + } + } else { + $passwordModal.modal(); + } }; /** - * set the format on bootstrap templates + * decrypt using the password from the modal dialog * - * @name controller.setFormat + * @name controller.decryptPasswordModal * @function - * @param {Event} event */ - me.setFormat = function(event) + me.decryptPasswordModal = function() { - var target = $(event.target); - $('#pasteFormatter').val(target.data('format')); - $('#pasteFormatterDisplay').text(target.text()); + $passwordInput.val($passwordDecrypt.val()); + me.displayMessages(); + }; - if ($messagePreview.parent().hasClass('active')) { - me.viewPreview(event); - } + /** + * submit a password in the modal dialog + * + * @name controller.submitPasswordModal + * @function + * @param {Event} event + */ + me.submitPasswordModal = function(event) + { event.preventDefault(); + $passwordModal.modal('hide'); }; + /** - * set the language in a cookie and reload the page + * init status manager * - * @name controller.setLanguage + * preloads jQuery elements + * + * @name controller.init * @function - * @param {Event} event */ - me.setLanguage = function(event) + me.init = function() { - document.cookie = 'lang=' + $(event.target).data('lang'); - me.reloadPage(event); + $password = $('#password'); + $passwordInput = $('#passwordinput'); + $passwordModal = $('#passwordmodal'); + $passwordForm = $('#passwordform'); + $passwordDecrypt = $('#passworddecrypt'); + + // bind events + + // focus password input when it is shown + $passwordModal.on('shown.bs.modal', function () { + $passwordDecrypt.focus(); + }); + // handle modal password request on decryption + $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal); + $passwordForm.submit(me.submitPasswordModal); }; + return me; + })(window, document); + + /** + * Manage paste/message input + * + * @param {object} window + * @param {object} document + * @class + */ + var editor = (function (window, document) { + var me = {}; + + var $message, + $messageEdit, + $messagePreview; + /** * support input of tab character * - * @name controller.supportTabs + * @name editor.supportTabs * @function * @param {Event} event * @TODO doc what is @this here? + * @TODO replace this with $message ?? */ - me.supportTabs = function(event) + function supportTabs(event) { var keyCode = event.keyCode || event.which; // tab was pressed @@ -1625,16 +1367,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // put caret at right position again this.selectionStart = this.selectionEnd = start + 1; } - }; + } /** * view the editor tab * - * @name controller.viewEditor + * @name editor.viewEditor * @function * @param {Event} event */ - me.viewEditor = function(event) + function viewEditor(event) { $messagePreview.parent().removeClass('active'); $messageEdit.parent().addClass('active'); @@ -1642,16 +1384,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.stateNewPaste(); event.preventDefault(); - }; + } /** * view the preview tab * - * @name controller.viewPreview + * @name editor.viewPreview * @function * @param {Event} event */ - me.viewPreview = function(event) + function viewPreview(event) { $messageEdit.parent().removeClass('active'); $messagePreview.parent().addClass('active'); @@ -1660,273 +1402,757 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.formatPaste($('#pasteFormatter').val(), $message.val()); event.preventDefault(); - }; + } /** - * handle history (pop) state changes + * reset the editor view * - * currently this does only handle redirects to the home page. - * - * @name controller.historyChange + * @name editor.reset * @function - * @param {Event} event */ - me.historyChange = function(event) + me.reset = function() { - var currentLocation = helper.scriptLocation(); - if (event.originalEvent.state === null && // no state object passed - event.originalEvent.target.location.href === currentLocation && // target location is home page - window.location.href === currentLocation // and we are not already on the home page - ) { - // redirect to home page - window.location.href = currentLocation; - } + // clear content + $message.text(''); }; /** - * Forces opening the paste if the link does not do this automatically. - * - * This is necessary as browsers will not reload the page when it is - * already loaded (which is fake as it is set via history.pushState()). + * shows the editor * - * @name controller.pasteLinkClick + * @name editor.show * @function - * @param {Event} event */ - me.pasteLinkClick = function(event) + me.show = function() { - // check if location is (already) shown in URL bar - if (window.location.href === $pasteUrl.attr('href')) { - // if so we need to load link by reloading the current site - window.location.reload(true); - } + $attachment.removeClass('hidden'); + $clearText.removeClass('hidden'); + $discussion.removeClass('hidden'); + $pasteResult.removeClass('hidden'); //?? + // $prettyMessage.removeClass('hidden'); + $remainingTime.removeClass('hidden'); }; /** - * create a new paste + * hides the editor * - * @name controller.newPaste + * @name editor.reset * @function */ - me.newPaste = function() + me.hide = function() { - me.stateNewPaste(); - me.showStatus(''); - $message.text(''); - me.changeBurnAfterReading(); - me.changeOpenDisc(); + $attachment.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $pasteResult.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $remainingTime.addClass('hidden'); }; /** - * removes an attachment + * focuses the message input * - * @name controller.removeAttachment + * @name editor.focus * @function */ - me.removeAttachment = function() + me.focus = function() { - $clonedFile.addClass('hidden'); - // removes the saved decrypted file data - $attachmentLink.attr('href', ''); - // the only way to deselect the file is to recreate the input // @TODO really? - $fileWrap.html($fileWrap.html()); - $fileWrap.removeClass('hidden'); + $message.focus(); }; /** - * decrypt using the password from the modal dialog + * init status manager * - * @name controller.decryptPasswordModal + * preloads jQuery elements + * + * @name editor.init * @function */ - me.decryptPasswordModal = function() + me.init = function() { - $passwordInput.val($passwordDecrypt.val()); - me.displayMessages(); + $message = $('#message'); + $messageEdit = $('#messageedit'); + $messagePreview = $('#messagepreview'); + + // bind events + $message.keydown(supportTabs); + $messageEdit.click(viewEditor); + $messagePreview.click(viewPreview); }; - /** - * submit a password in the modal dialog - * - * @name controller.submitPasswordModal - * @function - * @param {Event} event - */ - me.submitPasswordModal = function(event) - { - event.preventDefault(); - $passwordModal.modal('hide'); + return me; + })(window, document); + + /** + * Manage top (navigation) bar + * + * @param {object} window + * @param {object} document + * @name state + * @class + */ + var topNav = (function (window, document) { + var me = {}; + + var $attach, + $attachment, + $attachmentLink, + $burnAfterReading, + $burnAfterReadingOption, + $cloneButton, + $expiration, + $fileRemoveButton, + $fileWrap, + $formatter, + $newButton, + $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear + $openDiscussion, + $rawTextButton, + $sendButton; + + /** + * set the expiration on bootstrap templates + * + * @name topNav.setExpiration + * @function + * @param {Event} event + */ + function setExpiration(event) + { + event.preventDefault(); + var target = $(event.target); + $('#pasteExpiration').val(target.data('expiration')); + $('#pasteExpirationDisplay').text(target.text()); + } + + /** + * set the format on bootstrap templates + * + * @name topNav.setFormat + * @function + * @param {Event} event + */ + me.setFormat = function(event) + { + var target = $(event.target); + $('#pasteFormatter').val(target.data('format')); + $('#pasteFormatterDisplay').text(target.text()); + + if ($messagePreview.parent().hasClass('active')) { + me.viewPreview(event); + } + event.preventDefault(); }; /** - * display an error message, - * we use the same function for paste and reply to comments + * when "burn after reading" is checked, disable discussion * - * @name controller.showError + * @name topNav.changeBurnAfterReading * @function - * @param {string} message - text to display */ - me.showError = function(message) + function changeBurnAfterReading() { - if ($status.length) + if ($burnAfterReading.is(':checked') ) { - $status.addClass('errorMessage').text(message); + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); } else { - $errorMessage.removeClass('hidden'); - helper.setMessage($errorMessage, message); - } - if (typeof $replyStatus !== 'undefined') { - $replyStatus.addClass('errorMessage'); - $replyStatus.addClass($errorMessage.attr('class')); - if ($status.length) - { - $replyStatus.html($status.html()); - } - else - { - $replyStatus.html($errorMessage.html()); - } + $openDisc.removeClass('buttondisabled'); + $openDiscussion.removeAttr('disabled'); } - }; + } /** - * display a status message, - * we use the same function for paste and reply to comments + * when discussion is checked, disable "burn after reading" * - * @name controller.showStatus + * @name topNav.changeOpenDisc * @function - * @param {string} message - text to display - * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false */ - me.showStatus = function(message, spin) + function changeOpenDisc() { - if (spin || false) - { - var img = ''; - $status.prepend(img); - if (typeof $replyStatus !== 'undefined') { - $replyStatus.prepend(img); - } - } - if (typeof $replyStatus !== 'undefined') { - $replyStatus.removeClass('errorMessage').text(message); - } - if (!message) + if ($openDiscussion.is(':checked') ) { - $status.html(' '); - return; + $burnAfterReadingOption.addClass('buttondisabled'); + $burnAfterReading.attr({checked: false, disabled: true}); } - if (message === '') + else { - $status.html(' '); - return; + $burnAfterReadingOption.removeClass('buttondisabled'); + $burnAfterReading.removeAttr('disabled'); } - $status.removeClass('errorMessage').text(message); + } + + /** + * return raw text + * + * @name topNav.rawText + * @function + * @param {Event} event + */ + function rawText(event) + { + var paste = $('#pasteFormatter').val() === 'markdown' ? + $prettyPrint.text() : $clearText.text(); + history.pushState( + null, document.title, helper.scriptLocation() + '?' + + helper.pasteId() + '#' + helper.pageKey() + ); + // we use text/html instead of text/plain to avoid a bug when + // reloading the raw text view (it reverts to type text/html) + var newDoc = document.open('text/html', 'replace'); + newDoc.write('
' + helper.htmlEntities(paste) + '
'); + newDoc.close(); + + event.preventDefault(); + } + + /** + * set the language in a cookie and reload the page + * + * @name topNav.setLanguage + * @function + * @param {Event} event + */ + function setLanguage(event) + { + document.cookie = 'lang=' + $(event.target).data('lang'); + me.reloadPage(event); + } + + /** + * removes an attachment + * + * @name controller.removeAttachment + * @function + */ + me.removeAttachment = function() + { + $clonedFile.addClass('hidden'); + // removes the saved decrypted file data + $attachmentLink.attr('href', ''); + // the only way to deselect the file is to recreate the input // @TODO really? + $fileWrap.html($fileWrap.html()); + $fileWrap.removeClass('hidden'); }; /** - * bind events to DOM elements + * Shows all elements belonging to viwing an existing pastes * - * @private + * @name topNav.hideAllElem * @function */ - function bindEvents() + me.showViewButtons = function() { - $burnAfterReading.change(me.changeBurnAfterReading); - $openDisc.change(me.changeOpenDisc); - $sendButton.click(me.sendData); - $cloneButton.click(me.clonePaste); - $rawTextButton.click(me.rawText); - $fileRemoveButton.click(me.removeAttachment); - $('.reloadlink').click(me.reloadPage); - $message.keydown(me.supportTabs); - $messageEdit.click(me.viewEditor); - $messagePreview.click(me.viewPreview); + $cloneButton.removeClass('hidden'); + $rawTextButton.removeClass('hidden'); + }; - // bootstrap template drop downs - $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration); - $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat); - $('#language ul.dropdown-menu li a').click(me.setLanguage); + /** + * Hides all elements belonging to existing pastes + * + * @name topNav.hideAllElem + * @function + */ + me.hideViewButtons = function() + { + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + }; - // page template drop down - $('#language select option').click(me.setLanguage); + /** + * shows all elements needed when creating a new paste + * + * @name topNav.setLanguage + * @function + */ + me.showCreateButtons = function() + { + $sendButton.removeClass('hidden'); + $expiration.removeClass('hidden'); + $formatter.removeClass('hidden'); + $burnAfterReadingOption.removeClass('hidden'); + $openDisc.removeClass('hidden'); + $newButton.removeClass('hidden'); + $password.removeClass('hidden'); + $attach.removeClass('hidden'); + $message.removeClass('hidden'); + $preview.removeClass('hidden'); + }; - // focus password input when it is shown - $passwordModal.on('shown.bs.modal', function () { - $passwordDecrypt.focus(); - }); - // handle modal password request on decryption - $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal); - $passwordForm.submit(me.submitPasswordModal); + /** + * shows all elements needed when creating a new paste + * + * @name topNav.setLanguage + * @function + */ + me.hideCreateButtons = function() + { + $sendButton.addClass('hidden'); + $expiration.addClass('hidden'); + $formatter.addClass('hidden'); + $burnAfterReadingOption.addClass('hidden'); + $openDisc.addClass('hidden'); + $newButton.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); + $preview.addClass('hidden'); + }; - $(window).on('popstate', me.historyChange); + /** + * only shows the "new paste" button + * + * @name topNav.setLanguage + * @function + */ + me.showNewPasteButton = function() + { + $newButton.addClass('hidden'); }; /** - * main application + * shows a loading message, optionally with a percentage * - * @name controller.init + * @name topNav.showLoading * @function + * @param {string} message + * @param {int} percentage */ - me.init = function() + me.showLoading = function(message, percentage) { - // hide "no javascript" message - $('#noscript').hide(); + // currently parameters are ignored + $loadingIndicator.removeClass('hidden'); + }; + + /** + * hides the loading message + * + * @name topNav.hideLoading + * @function + */ + me.hideLoading = function() + { + $loadingIndicator.removeClass('hidden'); + }; - // preload jQuery wrapped DOM elements and bind events + /** + * init navigation manager + * + * preloads jQuery elements + * + * @name topNav.init + * @function + */ + me.init = function() + { $attach = $('#attach'); $attachment = $('#attachment'); $attachmentLink = $('#attachment a'); $burnAfterReading = $('#burnafterreading'); $burnAfterReadingOption = $('#burnafterreadingoption'); - $cipherData = $('#cipherdata'); - $clearText = $('#cleartext'); $cloneButton = $('#clonebutton'); - $clonedFile = $('#clonedfile'); - $comments = $('#comments'); - $discussion = $('#discussion'); - $errorMessage = $('#errormessage'); $expiration = $('#expiration'); $fileRemoveButton = $('#fileremovebutton'); $fileWrap = $('#filewrap'); $formatter = $('#formatter'); - $image = $('#image'); - $loadingIndicator = $('#loadingindicator'); - $message = $('#message'); - $messageEdit = $('#messageedit'); - $messagePreview = $('#messagepreview'); $newButton = $('#newbutton'); $openDisc = $('#opendisc'); $openDiscussion = $('#opendiscussion'); - $password = $('#password'); - $passwordInput = $('#passwordinput'); - $passwordModal = $('#passwordmodal'); - $passwordForm = $('#passwordform'); - $passwordDecrypt = $('#passworddecrypt'); - $pasteResult = $('#pasteresult'); - // $pasteUrl is saved in sendDataContinue() if/after it is - // actually created - $prettyMessage = $('#prettymessage'); - $prettyPrint = $('#prettyprint'); - $preview = $('#preview'); $rawTextButton = $('#rawtextbutton'); - $remainingTime = $('#remainingtime'); - // $replyStatus is saved in openReply() $sendButton = $('#sendbutton'); - $status = $('#status'); - bindEvents(); - - // display status returned by php code, if any (eg. paste was properly deleted) - if ($status.text().length > 0) - { - me.showStatus($status.text()); - return; - } - // keep line height even if content empty - $status.html(' '); + // bootstrap template drop down + $('#language ul.dropdown-menu li a').click(me.setLanguage); + // page template drop down + $('#language select option').click(me.setLanguage); + + // bind events + $burnAfterReading.change(changeBurnAfterReading); + $openDisc.change(changeOpenDisc); + $sendButton.click(controller.sendData); + $cloneButton.click(controller.clonePaste); + $rawTextButton.click(me.rawText); + $fileRemoveButton.click(me.removeAttachment); + + // initiate default state of checkboxes + changeBurnAfterReading(); + changeOpenDisc(); + }; + + return me; + })(window, document); + + /** + * PrivateBin logic + * + * @param {object} window + * @param {object} document + * @name controller + * @class + */ + var controller = (function (window, document) { + var me = {}; + + /** + * headers to send in AJAX requests + * + * @private + * @enum {Object} + */ + var headers = {'X-Requested-With': 'JSONHttpRequest'}; + + /** + * URL shortners create address + * + * @private + * @prop {string} + */ + var shortenerUrl = ''; + + /** + * URL of newly created paste + * + * @private + * @prop {string} + */ + var createdPasteUrl = ''; + + /** + * send a reply in a discussion + * + * @name controller.sendComment + * @function + * @param {Event} event + */ + me.sendComment = function(event) + { + event.preventDefault(); + $errorMessage.addClass('hidden'); + // do not send if no data + var replyMessage = $('#replymessage'); + if (replyMessage.val().length === 0) + { + return; + } + + me.showStatus(i18n._('Sending comment...'), true); + var parentid = event.data.parentid, + key = helper.pageKey(), + cipherdata = cryptToolcipher(key, $passwordInput.val(), replyMessage.val()), + ciphernickname = '', + nick = $('#nickname').val(); + if (nick.length > 0) + { + ciphernickname = cryptToolcipher(key, $passwordInput.val(), nick); + } + var data_to_send = { + data: cipherdata, + parentid: parentid, + pasteid: helper.pasteId(), + nickname: ciphernickname + }; + + $.ajax({ + type: 'POST', + url: helper.scriptLocation(), + data: data_to_send, + dataType: 'json', + headers: headers, + success: function(data) + { + if (data.status === 0) + { + controller.showStatus(i18n._('Comment posted.')); + $.ajax({ + type: 'GET', + url: helper.scriptLocation() + '?' + helper.pasteId(), + dataType: 'json', + headers: headers, + success: function(data) + { + if (data.status === 0) + { + controller.displayMessages(data); + } + else if (data.status === 1) + { + controller.showError(i18n._('Could not refresh display: %s', data.message)); + } + else + { + controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); + } + } + }) + .fail(function() { + controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); + }); + } + else if (data.status === 1) + { + controller.showError(i18n._('Could not post comment: %s', data.message)); + } + else + { + controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); + } + } + }) + .fail(function() { + controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); + }); + }; + + /** + * send a new paste to server + * + * @name controller.sendData + * @function + * @param {Event} event + */ + me.sendData = function(event) + { + event.preventDefault(); + var file = document.getElementById('file'), + files = (file && file.files) ? file.files : null; // FileList object + + // do not send if no data. + if ($message.val().length === 0 && !(files && files[0])) + { + return; + } + + // if sjcl has not collected enough entropy yet, display a message + if (!sjcl.random.isReady()) + { + me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true); + sjcl.random.addEventListener('seeded', function() { + me.sendData(event); + }); + return; + } + + $('.navbar-toggle').click(); + $password.addClass('hidden'); + me.showStatus(i18n._('Sending paste...'), true); + + me.stateSubmittingPaste(); + + var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0), + password = $passwordInput.val(); + if(files && files[0]) + { + if(typeof FileReader === undefined) + { + // revert loading status… + me.stateNewPaste(); + me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + return; + } + var reader = new FileReader(); + // closure to capture the file information + reader.onload = (function(theFile) + { + return function(e) { + controller.sendDataContinue( + randomkey, + cryptToolcipher(randomkey, password, e.target.result), + cryptToolcipher(randomkey, password, theFile.name) + ); + }; + })(files[0]); + reader.readAsDataURL(files[0]); + } + else if($attachmentLink.attr('href')) + { + me.sendDataContinue( + randomkey, + cryptToolcipher(randomkey, password, $attachmentLink.attr('href')), + $attachmentLink.attr('download') + ); + } + else + { + me.sendDataContinue(randomkey, '', ''); + } + }; + + /** + * send a new paste to server, step 2 + * + * @name controller.sendDataContinue + * @function + * @param {string} randomkey + * @param {string} cipherdata_attachment + * @param {string} cipherdata_attachment_name + */ + me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) + { + var cipherdata = cryptToolcipher(randomkey, $passwordInput.val(), $message.val()), + data_to_send = { + data: cipherdata, + expire: $('#pasteExpiration').val(), + formatter: $('#pasteFormatter').val(), + burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0, + opendiscussion: $openDiscussion.is(':checked') ? 1 : 0 + }; + if (cipherdata_attachment.length > 0) + { + data_to_send.attachment = cipherdata_attachment; + if (cipherdata_attachment_name.length > 0) + { + data_to_send.attachmentname = cipherdata_attachment_name; + } + } + $.ajax({ + type: 'POST', + url: helper.scriptLocation(), + data: data_to_send, + dataType: 'json', + headers: headers, + success: function(data) + { + if (data.status === 0) { + me.stateExistingPaste(); + var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, + deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; + me.hideStatus(); + $errorMessage.addClass('hidden'); + // show new URL in browser bar + history.pushState({type: 'newpaste'}, document.title, url); + + $('#pastelink').html( + i18n._( + 'Your paste is %s (Hit [Ctrl]+[c] to copy)', + url, url + ) + me.shortenUrl(url) + ); + // save newly created element + $pasteUrl = $('#pasteurl'); + // and add click event + $pasteUrl.click(me.pasteLinkClick); + + var shortenButton = $('#shortenbutton'); + if (shortenButton) { + shortenButton.click(me.sendToShortener); + } + $('#deletelink').html('' + i18n._('Delete data') + ''); + $pasteResult.removeClass('hidden'); + // we pre-select the link so that the user only has to [Ctrl]+[c] the link + helper.selectText($pasteUrl[0]); + me.hideStatus(); + me.formatPaste(data_to_send.formatter, $message.val()); + } + else if (data.status === 1) + { + // revert loading status… + controller.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', data.message)); + } + else + { + // revert loading status… + controller.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); + } + } + }) + .fail(function() + { + // revert loading status… + me.stateNewPaste(); + controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); + }); + }; + + /** + * check if a URL shortener was defined and create HTML containing a link to it + * + * @name controller.shortenUrl + * @function + * @param {string} url + * @return {string} html + */ + me.shortenUrl = function(url) + { + var shortenerHtml = $('#shortenbutton'); + if (shortenerHtml) { + shortenerUrl = shortenerHtml.data('shortener'); + createdPasteUrl = url; + return ' ' + $('
').append(shortenerHtml.clone()).html(); + } + return ''; + }; + + /** + * forward to URL shortener + * + * @name controller.sendToShortener + * @function + * @param {Event} event + */ + me.sendToShortener = function(event) + { + window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl); + event.preventDefault(); + }; + + /** + * clone the current paste + * + * @name controller.clonePaste + * @function + * @param {Event} event + */ + me.clonePaste = function(event) + { + me.stateNewPaste(); + + // erase the id and the key in url + history.replaceState(null, document.title, helper.scriptLocation()); + + status.hideStatus(); + if ($attachmentLink.attr('href')) + { + $clonedFile.removeClass('hidden'); + $fileWrap.addClass('hidden'); + } + $message.text( + $('#pasteFormatter').val() === 'markdown' ? + $prettyPrint.text() : $clearText.text() + ); + $('.navbar-toggle').click(); + + event.preventDefault(); + }; + + /** + * create a new paste + * + * @name controller.newPaste + * @function + */ + me.newPaste = function() + { + me.stateNewPaste(); + me.hideStatus(); + $message.text(''); + }; + + /** + * application start + * + * @name controller.init + * @function + */ + me.init = function() + { + // first load translations + i18n.loadTranslations(); + + // init UI @TODO show loading + uiMan.init(); // display an existing paste if ($cipherData.text().length > 1) @@ -1959,9 +2185,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * main application start, called when DOM is fully loaded and - * runs controller initalization after translations are loaded + * runs controller initalization */ - $(i18n.loadTranslations); + $(controller.init); return { helper: helper, diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 698d359..a2f5272 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -404,19 +404,14 @@ if ($FILEUPLOAD):
-
-
+ if ($isCpct): + ?>
+
+
- + - + - + - - - - - -
+
+ +
+
+
+

-

+

+

+ in the browser using 256 bits AES. More information on the project page.', I18n::_($NAME)), PHP_EOL; ?> +

+
+
+
+ From b0876ea0e03595af0066e8d29a044a84adf4e289 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 17 Feb 2017 21:48:21 +0100 Subject: [PATCH 15/59] :bug: Fix error not appearing below comment --- js/privatebin.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/privatebin.js b/js/privatebin.js index 8c5a785..55350af 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -3426,11 +3426,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.setFailure(function (status, data) { // revert loading status… Alert.hideLoading(); - Alert.setCustomHandler(null); TopNav.showViewButtons(); // show error message Alert.showError(Uploader.parseUploadError(status, data, 'post comment')); + + // reset error handler + Alert.setCustomHandler(null); }); // fill it with unencrypted params From 52d1be1b5442884ccd17a7963ddec86ed1593060 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 17 Feb 2017 22:26:39 +0100 Subject: [PATCH 16/59] Fix https://github.com/PrivateBin/PrivateBin/issues/187 --- js/privatebin.js | 35 ++++++++++++++++++++++++++--------- tpl/bootstrap.php | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 55350af..bffb978 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2560,6 +2560,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ function rawText(event) { + TopNav.hideAllButtons(); + Alert.showLoading('Showing raw text…', 0, 'time'); var paste = PasteViewer.getText(); // push a new state to allow back navigation with browser back button @@ -2573,11 +2575,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // we use text/html instead of text/plain to avoid a bug when // reloading the raw text view (it reverts to type text/html) + var $head = $('head').children().not('noscript, script, link[type="text/css"]'); var newDoc = document.open('text/html', 'replace'); - newDoc.write('
' + Helper.htmlEntities(paste) + '
'); + newDoc.write(''); + for (var i = 0; i < $head.length; i++) { + newDoc.write($head[i].outerHTML); + } + newDoc.write('
' + Helper.htmlEntities(paste) + '
'); newDoc.close(); - - event.preventDefault(); } /** @@ -2643,7 +2648,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Shows all elements belonging to viwing an existing pastes * - * @name TopNav.hideAllElem + * @name TopNav.showViewButtons * @function */ me.showViewButtons = function() @@ -2663,7 +2668,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Hides all elements belonging to existing pastes * - * @name TopNav.hideAllElem + * @name TopNav.hideViewButtons * @function */ me.hideViewButtons = function() @@ -2680,6 +2685,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { viewButtonsDisplayed = false; }; + /** + * Hides all elements belonging to existing pastes + * + * @name TopNav.hideAllButtons + * @function + */ + me.hideAllButtons = function() + { + me.hideViewButtons(); + me.hideCreateButtons(); + }; + /** * shows all elements needed when creating a new paste * @@ -3393,7 +3410,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.setCustomHandler(DiscussionViewer.handleNotification); // UI loading state - TopNav.hideViewButtons(); + TopNav.hideAllButtons(); Alert.showLoading('Sending comment…', 0, 'cloud-upload'); // get data @@ -3467,7 +3484,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Controller.hideStatusMessages(); // UI loading state - TopNav.hideCreateButtons(); + TopNav.hideAllButtons(); Alert.showLoading('Sending paste…', 0, 'cloud-upload'); TopNav.collapseBar(); @@ -3826,7 +3843,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { // Important: This *must not* run Alert.hideMessages() as previous // errors from viewing a paste should be shown. - TopNav.hideViewButtons(); + TopNav.hideAllButtons(); Alert.showLoading('Preparing new paste…', 0, 'time'); PasteStatus.hideMessages(); @@ -3911,7 +3928,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.clonePaste = function(event) { TopNav.collapseBar(); - TopNav.hideViewButtons(); + TopNav.hideAllButtons(); Alert.showLoading('Cloning paste…', 0, 'transfer'); // hide messages from previous paste diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 7f07bcb..9b9f9eb 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -4,7 +4,7 @@ $isCpct = substr($template, 9, 8) === '-compact'; $isDark = substr($template, 9, 5) === '-dark'; $isPage = substr($template, -5) === '-page'; ?> - + From c03377577995b6941e4c9f93e8d3fb73582e3be5 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 17 Feb 2017 22:46:18 +0100 Subject: [PATCH 17/59] Cleanup --- js/privatebin.js | 91 ++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index bffb978..2ee6216 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -986,8 +986,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var me = {}; var $errorMessage, - $statusMessage, - $loadingIndicator; + $loadingIndicator, + $statusMessage; var currentIcon = [ 'glyphicon-time', // loading icon @@ -1220,8 +1220,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // not a reset, but first set of the elements $errorMessage = $('#errormessage'); - $statusMessage = $('#status'); $loadingIndicator = $('#loadingindicator'); + $statusMessage = $('#status'); // display status returned by php code, if any (e.g. paste was properly deleted) var serverStatus = $statusMessage.text(); @@ -1250,9 +1250,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var me = {}; var $pasteSuccess, - $shortenButton, $pasteUrl, - $remainingTime; + $remainingTime, + $shortenButton; /** * forward to URL shortener @@ -1378,10 +1378,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $shortenButton = $('#shortenbutton'); $pasteSuccess = $('#pasteSuccess'); // $pasteUrl is saved in me.createPasteNotification() after creation $remainingTime = $('#remainingtime'); + $shortenButton = $('#shortenbutton'); // bind elements $shortenButton.click(sendToShortener); @@ -1400,9 +1400,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var Prompt = (function (window, document) { var me = {}; - var $passwordModal, + var $passwordDecrypt, $passwordForm, - $passwordDecrypt; + $passwordModal; var password = '', passwordCallback = null; @@ -1504,9 +1504,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $passwordModal = $('#passwordmodal'); - $passwordForm = $('#passwordform'); $passwordDecrypt = $('#passworddecrypt'); + $passwordForm = $('#passwordform'); + $passwordModal = $('#passwordmodal'); // bind events @@ -1533,10 +1533,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var Editor = (function (window, document) { var me = {}; - var $message, + var $editorTabs, $messageEdit, $messagePreview, - $editorTabs; + $message; var isPreview = false; @@ -1722,8 +1722,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $message = $('#message'); $editorTabs = $('#editorTabs'); + $message = $('#message'); // bind events $message.keydown(supportTabs); @@ -1747,10 +1747,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var PasteViewer = (function (window, document) { var me = {}; - var $plainText, - $placeholder, + var $placeholder, $prettyMessage, - $prettyPrint; + $prettyPrint, + $plainText; var text, format = 'plaintext', @@ -1961,8 +1961,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $plainText = $('#plaintext'); $placeholder = $('#placeholder'); + $plainText = $('#plaintext'); $prettyMessage = $('#prettymessage'); $prettyPrint = $('#prettyprint'); @@ -1997,9 +1997,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var AttachmentViewer = (function (window, document) { var me = {}; - var $attachment, - $attachmentLink, - $attachmentPreview; + var $attachmentLink, + $attachmentPreview, + $attachment; var attachmentChanged = false, attachmentHasPreview = false; @@ -2148,9 +2148,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $attachmentPreview = $('#attachmentPreview'); $attachment = $('#attachment'); $attachmentLink = $('#attachment a'); + $attachmentPreview = $('#attachmentPreview'); }; return me; @@ -2166,13 +2166,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var DiscussionViewer = (function (window, document) { var me = {}; - var $commentContainer, - $commentTail, + var $commentTail, $discussion, $reply, $replyMessage, $replyNickname, - $replyStatus; + $replyStatus, + $commentContainer; var replyCommentId; @@ -2457,15 +2457,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $customAttachment, $expiration, $fileRemoveButton, + $fileWrap; $formatter, $newButton, - $openDiscussionOption, $openDiscussion, + $openDiscussionOption, $password, $passwordInput, $rawTextButton, - $sendButton, - $fileWrap; + $sendButton; var pasteExpiration = '1week'; @@ -2922,15 +2922,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $customAttachment = $('#customattachment'); $expiration = $('#expiration'); $fileRemoveButton = $('#fileremovebutton'); + $fileWrap = $('#filewrap'); $formatter = $('#formatter'); $newButton = $('#newbutton'); - $openDiscussionOption = $('#opendiscussionoption'); $openDiscussion = $('#opendiscussion'); + $openDiscussionOption = $('#opendiscussionoption'); $password = $('#password'); $passwordInput = $('#passwordinput'); $rawTextButton = $('#rawtextbutton'); $sendButton = $('#sendbutton'); - $fileWrap = $('#filewrap'); // bootstrap template drop down $('#language ul.dropdown-menu li a').click(setLanguage); @@ -4000,19 +4000,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // initialize other modules/"classes" Alert.init(); - Uploader.init(); Model.init(); + + AttachmentViewer.init(); CryptTool.init(); - UiHelper.init(); - TopNav.init(); + DiscussionViewer.init(); Editor.init(); + PasteDecrypter.init(); + PasteEncrypter.init(); PasteStatus.init(); PasteViewer.init(); - AttachmentViewer.init(); - DiscussionViewer.init(); - PasteEncrypter.init(); - PasteDecrypter.init(); Prompt.init(); + TopNav.init(); + UiHelper.init(); + Uploader.init(); // display an existing paste if (Model.hasCipherData()) { @@ -4027,16 +4028,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(window, document); return { - Editor: Editor, Helper: Helper, I18n: I18n, CryptTool: CryptTool, - TopNav: TopNav, + Model: Model, + UiHelper: UiHelper, Alert: Alert, - Uploader: Uploader, - Controller: Controller, - DiscussionViewer: DiscussionViewer, + PasteStatus: PasteStatus, + Prompt: Prompt, + Editor: Editor, PasteViewer: PasteViewer, - AttachmentViewer: AttachmentViewer + AttachmentViewer: AttachmentViewer, + DiscussionViewer: DiscussionViewer, + TopNav: TopNav, + Uploader: Uploader, + PasteEncrypter: PasteEncrypter, + PasteDecrypter: PasteDecrypter, + Controller: Controller }; }(jQuery, sjcl, Base64, RawDeflate); From 601aa5e3dc4765f22c9a9770776ce893217804ea Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 17 Feb 2017 22:59:16 +0100 Subject: [PATCH 18/59] :bug: Fix typo --- js/privatebin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/privatebin.js b/js/privatebin.js index 2ee6216..5a75d1c 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2457,7 +2457,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $customAttachment, $expiration, $fileRemoveButton, - $fileWrap; + $fileWrap, $formatter, $newButton, $openDiscussion, From c45e79142e570501d12d52043af714f968e4cb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Le=C3=A3o?= Date: Sat, 18 Feb 2017 08:51:20 -0200 Subject: [PATCH 19/59] Add missing translations Some of the strings were not published. --- i18n/pt.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/i18n/pt.json b/i18n/pt.json index 942281d..3e41890 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -6,6 +6,7 @@ "Porque a ignorância é uma benção", "en": "pt", "Paste does not exist, has expired or has been deleted.": + "A cópia não existe, expirou ou já foi excluída.", "%s requires php 5.3.0 or above to work. Sorry.": "%s requer php 5.3.0 ou superior para funcionar. Desculpa.", "%s requires configuration section [%s] to be present in configuration file.": @@ -17,6 +18,7 @@ "Invalid data.": "Dados inválidos.", "You are unlucky. Try again.": + "Você é azarado. Tente novamente", "Error saving comment. Sorry.": "Erro ao salvar comentário. Desculpa.", "Error saving paste. Sorry.": @@ -28,11 +30,13 @@ "Wrong deletion token. Paste was not deleted.": "Token de remoção inválido. A cópia não foi excluída.", "Paste was properly deleted.": + "A cópia foi devidamente excluída.", "JavaScript is required for %s to work.
Sorry for the inconvenience.": "JavaScript é necessário para que %s funcione.
Pedimos desculpas pela inconveniência.", "%s requires a modern browser to work.": "%s requer um navegador moderno para funcionar.", "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": + "Ainda usando Internet Explorer? Faça-se um favor, mude para um navegador moderno:", "New": "Novo", "Send": @@ -42,7 +46,9 @@ "Raw text": "Texto sem formato", "Expires": + "Expirar em", "Burn after reading": + "Queime após ler", "Open discussion": "Discussão aberta", "Password (recommended)": @@ -103,6 +109,7 @@ "unknown status": "Estado desconhecido", "server error or not responding": + "Servidor em erro ou não responsivo", "Could not post comment: %s": "Não foi possível publicar o comentário: %s", "Sending paste (Please move your mouse for more entropy)...": @@ -116,6 +123,7 @@ "Could not create paste: %s": "Não foi possível criar cópia: %s", "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": + "Não foi possível decifrar a cópia: chave de decriptografia ausente na URL (Você utilizou um redirecionador ou encurtador de URL que removeu parte dela?)", "Format": "Formato", "Plain Text": "Texto sem formato", "Source Code": "Código fonte", @@ -139,4 +147,5 @@ "Digite a senha", "Loading…": "Carregando…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": + "Caso essa mensagem nunca desapareça, por favor veja este FAQ para saber como resolver os problemas." } From db307c3a7770044c5e6e52d40188262797fdf1ce Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 22 Feb 2017 21:42:14 +0100 Subject: [PATCH 20/59] updated test cases and delete logic to properly implement documented API, thanks @r4sas #188 --- lib/PrivateBin.php | 28 ++++++++++++++++------------ tst/JsonApiTest.php | 3 +-- tst/PrivateBinTest.php | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index e754016..fc69e57 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -334,19 +334,16 @@ class PrivateBin // accessing this property ensures that the paste would be // deleted if it has already expired $burnafterreading = $paste->isBurnafterreading(); - if ($deletetoken == 'burnafterreading') { - if ($burnafterreading) { - $paste->delete(); - $this->_return_message(0, $dataid); - } else { - $this->_return_message(1, 'Paste is not of burn-after-reading type.'); - } + if ( + ($burnafterreading && $deletetoken == 'burnafterreading') || + Filter::slowEquals($deletetoken, $paste->getDeleteToken()) + ) { + // Paste exists and deletion token is valid: Delete the paste. + $paste->delete(); + $this->_status = 'Paste was properly deleted.'; } else { - // Make sure the token is valid. - if (Filter::slowEquals($deletetoken, $paste->getDeleteToken())) { - // Paste exists and deletion token is valid: Delete the paste. - $paste->delete(); - $this->_status = 'Paste was properly deleted.'; + if (!$burnafterreading && $deletetoken == 'burnafterreading') { + $this->_error = 'Paste is not of burn-after-reading type.'; } else { $this->_error = 'Wrong deletion token. Paste was not deleted.'; } @@ -357,6 +354,13 @@ class PrivateBin } catch (Exception $e) { $this->_error = $e->getMessage(); } + if ($this->_request->isJsonApiCall()) { + if (strlen($this->_error)) { + $this->_return_message(1, $this->_error); + } else { + $this->_return_message(0, $dataid); + } + } } /** diff --git a/tst/JsonApiTest.php b/tst/JsonApiTest.php index 8579f01..5cf1360 100644 --- a/tst/JsonApiTest.php +++ b/tst/JsonApiTest.php @@ -147,10 +147,9 @@ class JsonApiTest extends PHPUnit_Framework_TestCase $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data'); $paste = $this->_model->read(Helper::getPasteId()); $_POST = array( - 'action' => 'delete', + 'pasteid' => Helper::getPasteId(), 'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste->meta->salt), ); - $_SERVER['QUERY_STRING'] = Helper::getPasteId(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; $_SERVER['REQUEST_METHOD'] = 'POST'; ob_start(); diff --git a/tst/PrivateBinTest.php b/tst/PrivateBinTest.php index 355b3f9..cebda5a 100644 --- a/tst/PrivateBinTest.php +++ b/tst/PrivateBinTest.php @@ -1047,7 +1047,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase ob_end_clean(); $response = json_decode($content, true); $this->assertEquals(1, $response['status'], 'outputs status'); - $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted'); + $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after failing to delete data'); } /** From 131e08ca33f49f0cbbc435f099437b2d06e46af4 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 25 Feb 2017 09:35:55 +0100 Subject: [PATCH 21/59] made phpUnit and most mocha tests work again, had to remove some injected objects and added a helper method to facilitate a cache reset for the unit tests. Page template is still broken and the JS test for baseUri() fails --- js/privatebin.js | 260 +++++++++++++++++++++-------------------- js/test.js | 57 ++++----- tpl/bootstrap.php | 29 +++-- tpl/page.php | 4 +- tst/PrivateBinTest.php | 32 ++--- tst/ViewTest.php | 10 +- 6 files changed, 206 insertions(+), 186 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 5a75d1c..1310e47 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -104,7 +104,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } v = Math.floor(seconds / (60 * 60 * 24 * 30)); return [v, 'month']; - }; + } /** * checks if a string is valid text (and not onyl whitespace) @@ -117,7 +117,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isValidText = function(string) { return (string.length > 0 && $.trim(string) !== '') - }; + } /** * text range selection @@ -143,7 +143,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { selection.removeAllRanges(); selection.addRange(range); } - }; + } /** * set text of a jQuery element (required for IE), @@ -165,7 +165,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $element.text(text); } - }; + } /** * convert URLs to clickable links. @@ -195,7 +195,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { markup ) ); - }; + } /** * minimal sprintf emulation for %s and %d formats @@ -235,7 +235,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return val; }); - }; + } /** * get value of cookie, if it was set, empty string otherwise @@ -261,7 +261,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } return ''; - }; + } /** * get the current location (without search or hash part of the URL), @@ -289,7 +289,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return baseUri; - }; + } /** * convert all applicable characters to HTML entities @@ -305,7 +305,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /[&<>"'`=\/]/g, function(s) { return entityMap[s]; }); - }; + } return me; })(window, document); @@ -369,7 +369,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me._ = function() { return me.translate.apply(this, arguments); - }; + } /** * translate a string @@ -467,7 +467,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return output; - }; + } /** * per language functions to use to determine the plural form @@ -495,7 +495,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { default: return (n !== 1 ? 1 : 0); } - }; + } /** * load translations into cache @@ -539,7 +539,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); language = 'en'; }); - }; + } return me; })(window, document); @@ -603,7 +603,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return sjcl.encrypt(key, compress(message), options); } return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options); - }; + } /** * decrypt message with key, then decompress @@ -629,7 +629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } return ''; - }; + } /** * checks whether the crypt tool is ready. @@ -641,7 +641,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isEntropyReady = function() { return sjcl.random.isReady(); - }; + } /** * checks whether the crypt tool is ready. @@ -653,7 +653,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.addEntropySeedListener = function(func) { sjcl.random.addEventListener('seeded', func); - }; + } /** * returns a random symmetric key @@ -665,7 +665,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getSymmetricKey = function(func) { return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0); - }; + } /** * initialize crypt tool @@ -682,7 +682,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // I18n._('Messages cannot be decrypted or encrypted.') // ); // } - }; + } return me; })(); @@ -690,11 +690,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * (Model) Data source (aka MVC) * - * @param {object} window - * @param {object} document * @class */ - var Model = (function (window, document) { + var Model = (function () { var me = {}; var $cipherData, @@ -713,7 +711,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getExpirationDefault = function() { return $('#pasteExpiration').val(); - }; + } /** * returns the format set in the HTML @@ -726,7 +724,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getFormatDefault = function() { return $('#pasteFormatter').val(); - }; + } /** * check if cipher data was supplied @@ -738,7 +736,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hasCipherData = function() { return (me.getCipherData().length > 0); - }; + } /** * returns the cipher data @@ -750,7 +748,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getCipherData = function() { return $cipherData.text(); - }; + } /** * get the pastes unique identifier from the URL, @@ -772,7 +770,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return id; - }; + } /** * return the deciphering key stored in anchor part of the URL @@ -798,11 +796,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { symmetricKey = symmetricKey.substring(0, ampersandPos); } - } return symmetricKey; - }; + } /** * returns a jQuery copy of the HTML template @@ -820,6 +817,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return $element.prop('id', name); } + /** + * resets state, used for unit testing + * + * @name Model.reset + * @function + */ + me.reset = function() + { + $cipherData = $templates = id = symmetricKey = null; + } + /** * init navigation manager @@ -833,10 +841,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $cipherData = $('#cipherdata'); $templates = $('#templates'); - }; + } return me; - })(window, document); + })(); /** * Helper functions for user interface @@ -869,7 +877,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // redirect to home page window.location.href = currentLocation; } - }; + } /** * reload the page @@ -882,7 +890,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.reloadHome = function() { window.location.href = Helper.baseUri(); - }; + } /** * checks whether the element is currently visible in the viewport (so @@ -903,7 +911,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var viewportBottom = viewportTop + $(window).height(); return (elementTop > viewportTop && elementTop < viewportBottom); - }; + } /** * scrolls to a specific element @@ -956,7 +964,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } next(); }); - }; + } /** * initialize @@ -970,7 +978,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $('.reloadlink').prop('href', Helper.baseUri()); $(window).on('popstate', historyChange); - }; + } return me; })(window, document); @@ -1095,7 +1103,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // @TODO: implement autoclose handleNotification(1, $statusMessage, message, icon); - }; + } /** * display an error message @@ -1120,7 +1128,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // @TODO: implement autoclose handleNotification(3, $errorMessage, message, icon); - }; + } /** * shows a loading message, optionally with a percentage @@ -1151,7 +1159,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show loading status (cursor) $('body').addClass('loading'); - }; + } /** * hides the loading message @@ -1165,7 +1173,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // hide loading cursor $('body').removeClass('loading'); - }; + } /** * hides any status/error messages @@ -1180,7 +1188,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // also possible: $('.statusmessage').addClass('hidden'); $statusMessage.addClass('hidden'); $errorMessage.addClass('hidden'); - }; + } /** * set a custom handler, which gets all notifications. @@ -1203,7 +1211,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setCustomHandler = function(newHandler) { customHandler = newHandler; - }; + } /** * init status manager @@ -1234,7 +1242,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (Helper.isValidText(serverError)) { Alert.showError(); } - }; + } return me; })(window, document); @@ -1296,7 +1304,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.createPasteNotification = function(url, deleteUrl) { - $('#pastelink').find(':first').html( + $('#pastelink').html( I18n._( 'Your paste is %s (Hit [Ctrl]+[c] to copy)', url, url @@ -1314,7 +1322,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $pasteSuccess.removeClass('hidden'); // we pre-select the link so that the user only has to [Ctrl]+[c] the link Helper.selectText($pasteUrl[0]); - }; + } /** * shows the remaining time @@ -1354,7 +1362,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // in the end, display notification $remainingTime.removeClass('hidden'); - }; + } /** * hides the remaining time and successful upload notification @@ -1366,7 +1374,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $remainingTime.addClass('hidden'); $pasteSuccess.addClass('hidden'); - }; + } /** * init status manager @@ -1385,7 +1393,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // bind elements $shortenButton.click(sendToShortener); - }; + } return me; })(window, document); @@ -1442,7 +1450,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (passwordCallback !== null) { passwordCallback(); } - }; + } /** * getthe cached password @@ -1457,7 +1465,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getPassword = function() { return password; - }; + } /** * setsthe callback called when password is entered @@ -1469,7 +1477,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setPasswordCallback = function(callback) { passwordCallback = callback; - }; + } /** * submit a password in the modal dialog @@ -1516,7 +1524,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }); // handle Model password submission $passwordForm.submit(submitPasswordModal); - }; + } return me; })(window, document); @@ -1651,7 +1659,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // clear content $message.val(''); - }; + } /** * shows the Editor @@ -1663,7 +1671,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $message.removeClass('hidden'); $editorTabs.removeClass('hidden'); - }; + } /** * hides the Editor @@ -1675,7 +1683,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { $message.addClass('hidden'); $editorTabs.addClass('hidden'); - }; + } /** * focuses the message input @@ -1686,7 +1694,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.focusInput = function() { $message.focus(); - }; + } /** * sets a new text @@ -1698,7 +1706,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setText = function(newText) { $message.val(newText); - }; + } /** * returns the current text @@ -1710,7 +1718,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getText = function() { return $message.val() - }; + } /** * init status manager @@ -1732,7 +1740,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // (li) $messageEdit = $('#messageedit').click(viewEditor).parent(); $messagePreview = $('#messagepreview').click(viewPreview).parent(); - }; + } return me; })(window, document); @@ -1860,7 +1868,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { format = newFormat; isChanged = true; - }; + } /** * returns the current format @@ -1872,7 +1880,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getFormat = function() { return format; - }; + } /** * returns whether the current view is pretty printed @@ -1884,7 +1892,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isPrettyPrinted = function() { return $prettyPrint.hasClass('prettyprinted'); - }; + } /** * sets the text to show @@ -1899,7 +1907,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { text = newText; isChanged = true; } - }; + } /** * gets the current cached text @@ -1911,7 +1919,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getText = function(newText) { return text; - }; + } /** * show/update the parsed text (preview) @@ -1930,7 +1938,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { showPaste(); isDisplayed = true; } - }; + } /** * hide parsed text (preview) @@ -1949,7 +1957,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $placeholder.addClass('hidden'); isDisplayed = false; - }; + } /** * init status manager @@ -1982,7 +1990,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // get default option from template/HTML or fall back to set value format = Model.getFormatDefault() || format; - }; + } return me; })(window, document); @@ -2032,7 +2040,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } attachmentChanged = true; - }; + } /** * displays the attachment @@ -2102,7 +2110,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hasAttachment = function() { return ($attachmentLink.prop('href') !== '') - }; + } /** * return the attachment @@ -2117,7 +2125,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attachmentLink.prop('href'), $attachmentLink.prop('download') ]; - }; + } /** * moves the attachment link to another element @@ -2136,7 +2144,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // update text I18n._($attachmentLink, label, $attachmentLink.attr('download')); - }; + } /** * initiate @@ -2151,7 +2159,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attachment = $('#attachment'); $attachmentLink = $('#attachment a'); $attachmentPreview = $('#attachmentPreview'); - }; + } return me; })(window, document); @@ -2224,7 +2232,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return $replyStatus; - }; + } /** * open the comment entry when clicking the "Reply" button of a comment @@ -2313,7 +2321,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // finally append comment $place.append($commentEntry); - }; + } /** * finishes the discussion area after last comment @@ -2328,7 +2336,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show discussions $discussion.removeClass('hidden'); - }; + } /** * shows the discussion area @@ -2339,7 +2347,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showDiscussion = function() { $discussion.removeClass('hidden'); - }; + } /** * removes the old discussion and prepares everything for creating a new @@ -2355,7 +2363,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // (re-)init templates initTemplates(); - }; + } /** * returns the user put into the reply form @@ -2370,7 +2378,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $replyMessage.val(), $replyNickname.val() ]; - }; + } /** * highlights a specific comment and scrolls to it if necessary @@ -2402,7 +2410,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } UiHelper.scrollTo($comment, 100, 'swing', highlightComment); - }; + } /** * returns the id of the parent comment the user is replying to @@ -2414,7 +2422,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getReplyCommentId = function() { return replyCommentId; - }; + } /** * initiate @@ -2432,7 +2440,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $commentContainer = $('#commentcontainer'); $discussion = $('#discussion'); - }; + } return me; })(window, document); @@ -2643,7 +2651,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.loadDefaults = function() { // @TODO - }; + } /** * Shows all elements belonging to viwing an existing pastes @@ -2663,7 +2671,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $rawTextButton.removeClass('hidden'); viewButtonsDisplayed = true; - }; + } /** * Hides all elements belonging to existing pastes @@ -2683,7 +2691,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $rawTextButton.addClass('hidden'); viewButtonsDisplayed = false; - }; + } /** * Hides all elements belonging to existing pastes @@ -2695,7 +2703,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { me.hideViewButtons(); me.hideCreateButtons(); - }; + } /** * shows all elements needed when creating a new paste @@ -2720,7 +2728,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attach.removeClass('hidden'); createButtonsDisplayed = true; - }; + } /** * shows all elements needed when creating a new paste @@ -2745,7 +2753,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attach.addClass('hidden'); createButtonsDisplayed = false; - }; + } /** * only shows the "new paste" button @@ -2756,7 +2764,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showNewPasteButton = function() { $newButton.removeClass('hidden'); - }; + } /** * only hides the clone button @@ -2767,7 +2775,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideCloneButton = function() { $cloneButton.addClass('hidden'); - }; + } /** * only hides the raw text button @@ -2778,7 +2786,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideRawButton = function() { $rawTextButton.addClass('hidden'); - }; + } /** * hides the file selector in attachment @@ -2789,7 +2797,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideFileSelector = function() { $fileWrap.addClass('hidden'); - }; + } /** @@ -2801,7 +2809,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showCustomAttachment = function() { $customAttachment.removeClass('hidden'); - }; + } /** * collapses the navigation bar if nedded @@ -2818,7 +2826,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // if so, toggle it $bar.click(); } - }; + } /** * returns the currently set expiration time @@ -2830,7 +2838,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getExpiration = function() { return pasteExpiration; - }; + } /** * returns the currently selected file(s) @@ -2853,7 +2861,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return $file[0].files; - }; + } /** * returns the state of the burn after reading checkbox @@ -2865,7 +2873,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getBurnAfterReading = function() { return $burnAfterReading.is(':checked'); - }; + } /** * returns the state of the discussion checkbox @@ -2877,7 +2885,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getOpenDiscussion = function() { return $openDiscussion.is(':checked'); - }; + } /** * returns the entered password @@ -2889,7 +2897,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getPassword = function() { return $passwordInput.val(); - }; + } /** * returns the element where custom attachments can be placed @@ -2903,7 +2911,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.getCustomAttachment = function() { return $customAttachment; - }; + } /** * init navigation manager @@ -2958,7 +2966,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { pasteExpiration = Model.getExpirationDefault() || pasteExpiration; me.loadDefaults(); - }; + } return me; })(window, document); @@ -3094,7 +3102,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { console.error(textStatus, errorThrown); fail(3, jqXHR); }); - }; + } /** * set success function @@ -3106,7 +3114,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUrl = function(newUrl) { url = newUrl; - }; + } /** * sets the password to use (first value) and optionally also the @@ -3126,7 +3134,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (typeof newKey !== 'undefined') { symmetricKey = newKey; } - }; + } /** * set success function @@ -3138,7 +3146,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setSuccess = function(func) { successFunc = func; - }; + } /** * set failure function @@ -3150,7 +3158,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setFailure = function(func) { failureFunc = func; - }; + } /** * prepares a new upload @@ -3178,7 +3186,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { failureFunc = null; url = Helper.baseUri(); data = {}; - }; + } /** * encrypts and sets the data @@ -3192,7 +3200,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { checkCryptParameters(); data[index] = CryptTool.cipher(symmetricKey, password, element); - }; + } /** * set the additional metadata to send unencrypted @@ -3205,7 +3213,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUnencryptedData = function(index, element) { data[index] = element; - }; + } /** * set the additional metadata to send unencrypted passed at once @@ -3217,7 +3225,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.setUnencryptedBulkData = function(newData) { $.extend(data, newData); - }; + } /** * Helper, which parses shows a general error message based on the result of the Uploader @@ -3258,7 +3266,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - }; + } return me; })(); @@ -3381,7 +3389,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // run callback return callback(); - }; + } // actually read first file reader.readAsDataURL(file); @@ -3413,8 +3421,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { TopNav.hideAllButtons(); Alert.showLoading('Sending comment…', 0, 'cloud-upload'); - // get data - var [plainText, nickname] = DiscussionViewer.getReplyData(), + // get data, note that "var [x, y] = " structures aren't supported in all JS environments + var replyData = DiscussionViewer.getReplyData(), + plainText = replyData[0], + nickname = replyData[1], parentid = DiscussionViewer.getReplyCommentId(); // do not send if there is no data @@ -3470,7 +3480,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } Uploader.run(); - }; + } /** * sends a new paste to server @@ -3548,7 +3558,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.run(); } ); - }; + } /** * initialize @@ -3559,7 +3569,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - }; + } return me; })(); @@ -3795,7 +3805,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.hideLoading(); TopNav.showViewButtons(); - }; + } /** * initialize @@ -3806,7 +3816,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.init = function() { // nothing yet - }; + } return me; })(); @@ -3831,7 +3841,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { PasteStatus.hideMessages(); Alert.hideMessages(); - }; + } /** * creates a new paste @@ -3855,7 +3865,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { TopNav.loadDefaults(); TopNav.showCreateButtons(); Alert.hideLoading(); - }; + } /** * shows the loaded paste @@ -3882,7 +3892,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show proper elements on screen PasteDecrypter.run(); return; - }; + } /** * refreshes the loaded paste to show potential new data @@ -3916,7 +3926,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { callback(); }) Uploader.run(); - }; + } /** * clone the current paste @@ -3964,7 +3974,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.hideLoading(); TopNav.showCreateButtons(); - }; + } /** * removes a saved paste @@ -3985,7 +3995,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }) Uploader.run(); - }; + } /** * application start @@ -4022,7 +4032,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // otherwise create a new paste me.newPaste(); - }; + } return me; })(window, document); diff --git a/js/test.js b/js/test.js index 80dc16a..66a44bb 100644 --- a/js/test.js +++ b/js/test.js @@ -20,7 +20,7 @@ global.RawDeflate = require('./rawdeflate-0.5'); require('./rawinflate-0.3'); require('./privatebin'); -describe('helper', function () { +describe('Helper', function () { describe('secondsToHuman', function () { after(function () { cleanup(); @@ -66,7 +66,7 @@ describe('helper', function () { }); }); - describe('scriptLocation', function () { + describe('baseUri', function () { jsc.property( 'returns the URL without query & fragment', jsc.nearray(jsc.elements(a2zString)), @@ -76,19 +76,36 @@ describe('helper', function () { function (schema, address, query, fragment) { var expected = schema.join('') + '://' + address.join('') + '/', clean = jsdom('', {url: expected + '?' + query.join('') + '#' + fragment}), - result = $.PrivateBin.Helper.scriptLocation(); + result = $.PrivateBin.Helper.baseUri(); clean(); return expected === result; } ); }); - describe('pasteId', function () { + describe('htmlEntities', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'removes all HTML entities from any given string', + 'string', + function (string) { + var result = $.PrivateBin.Helper.htmlEntities(string); + return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); + } + ); + }); +}); + +describe('Model', function () { + describe('getPasteId', function () { jsc.property( 'returns the query string without separator, if any', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), - jsc.array(jsc.elements(queryString)), + jsc.nearray(jsc.elements(queryString)), 'string', function (schema, address, query, fragment) { var queryString = query.join(''), @@ -96,27 +113,29 @@ describe('helper', function () { url: schema.join('') + '://' + address.join('') + '/?' + queryString + '#' + fragment }), - result = $.PrivateBin.Helper.pasteId(); + result = $.PrivateBin.Model.getPasteId(); + $.PrivateBin.Model.reset(); clean(); return queryString === result; } ); }); - describe('pageKey', function () { + describe('getPasteKey', function () { jsc.property( 'returns the fragment of the URL', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), - jsc.array(jsc.elements(base64String)), + jsc.nearray(jsc.elements(base64String)), function (schema, address, query, fragment) { var fragmentString = fragment.join(''), clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/?' + query.join('') + '#' + fragmentString }), - result = $.PrivateBin.Modal.getPasteKey(); + result = $.PrivateBin.Model.getPasteKey(); + $.PrivateBin.Model.reset(); clean(); return fragmentString === result; } @@ -126,7 +145,7 @@ describe('helper', function () { jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), - jsc.array(jsc.elements(base64String)), + jsc.nearray(jsc.elements(base64String)), jsc.array(jsc.elements(queryString)), function (schema, address, query, fragment, trail) { var fragmentString = fragment.join(''), @@ -134,25 +153,11 @@ describe('helper', function () { url: schema.join('') + '://' + address.join('') + '/?' + query.join('') + '#' + fragmentString + '&' + trail.join('') }), - result = $.PrivateBin.Modal.getPasteKey(); + result = $.PrivateBin.Model.getPasteKey(); + $.PrivateBin.Model.reset(); clean(); return fragmentString === result; } ); }); - - describe('htmlEntities', function () { - after(function () { - cleanup(); - }); - - jsc.property( - 'removes all HTML entities from any given string', - 'string', - function (string) { - var result = $.PrivateBin.Helper.htmlEntities(string); - return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); - } - ); - }); }); diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 05742bb..05caa3e 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + @@ -122,7 +122,7 @@ endif;
+if ($isCpct): +?>
- +
@@ -465,12 +464,18 @@ endif;
diff --git a/tpl/page.php b/tpl/page.php index 4ae0b6a..d7043bc 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + @@ -125,7 +125,7 @@ endif; - #s', + '#]*id="status"[^>]*>.*Paste was properly deleted\.assertRegExp( - '#]*id="errormessage"[^>]*>.*Invalid paste ID\.
#', + '#]*id="errormessage"[^>]*>.*Invalid paste ID\.assertRegExp( - '#]*id="errormessage"[^>]*>.*Paste does not exist[^<]*#', + '#]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.assertRegExp( - '#]*id="errormessage"[^>]*>.*Wrong deletion token[^<]*#', + '#]*id="errormessage"[^>]*>.*Wrong deletion token\. Paste was not deleted\.assertRegExp( - '#]*id="errormessage"[^>]*>.*Paste does not exist[^<]*#', + '#]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.assertRegExp( - '#]*id="status"[^>]*>.*Paste was properly deleted[^<]*#s', + '#]*id="status"[^>]*>.*Paste was properly deleted\.assertRegExp( - '#]+id="errormessage"[^>]*>.*' . self::$error . '#', + '#]+id="errormessage"[^>]*>.*' . self::$error . ' Date: Sun, 5 Mar 2017 11:02:18 +0100 Subject: [PATCH 26/59] credited Tulio for the portuguese translation, updated SRI hashes --- CHANGELOG.md | 2 +- CREDITS.md | 1 + js/privatebin.js | 2 +- lib/I18n.php | 2 +- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc2e3c..b10a3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # PrivateBin version history * **next (not yet released)** - * ADDED: Translations for Spanish, Occitan and Norwegian + * ADDED: Translations for Spanish, Occitan, Norwegian and Portuguese * ADDED: Option in configuration to change the default "PrivateBin" title of the site * CHANGED: Cleanup of bootstrap template variants and moved icons to `img` directory * **1.1 (2016-12-26)** diff --git a/CREDITS.md b/CREDITS.md index dfb2d83..1c7ec3c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -35,3 +35,4 @@ Sébastien Sauvage - original idea and main developer * Alfredo Fabián Altamirano Tena - Spanish * Quent-in - Occitan * idarlund - Norwegian +* Tulio Leao - Portuguese diff --git a/js/privatebin.js b/js/privatebin.js index 2fc8827..6508bec 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -450,7 +450,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)); case 'sl': return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0))); - // de, en, es, it, no + // de, en, es, it, no, pt default: return (n !== 1 ? 1 : 0); } diff --git a/lib/I18n.php b/lib/I18n.php index 4c59ef5..d35bcf0 100644 --- a/lib/I18n.php +++ b/lib/I18n.php @@ -304,7 +304,7 @@ class I18n return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); case 'sl': return $n % 100 == 1 ? 1 : ($n % 100 == 2 ? 2 : ($n % 100 == 3 || $n % 100 == 4 ? 3 : 0)); - // de, en, es, it, no + // de, en, es, it, no, pt default: return $n != 1 ? 1 : 0; } diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 4bf3ca1..04f2726 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + diff --git a/tpl/page.php b/tpl/page.php index 4ae0b6a..2bdbf91 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From bd32a73d21579cc547eb39f7340d96f12d7d40f7 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 5 Mar 2017 11:10:52 +0100 Subject: [PATCH 27/59] remove Safari link on bootstrap template, too --- tpl/bootstrap.php | 3 +-- tpl/page.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 04f2726..cdd4525 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -422,8 +422,7 @@ endif; Date: Mon, 13 Mar 2017 21:15:52 +0100 Subject: [PATCH 40/59] comply with codacys suggestion --- js/privatebin.js | 2 +- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index a7abee7..10b6a3a 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -3610,7 +3610,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { password = Prompt.getPassword(); // if password is there, re-try - if (password.length == 0) { + if (password.length === 0) { password = Prompt.requestPassword(); } // recursive diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 87def4c..a80e175 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + diff --git a/tpl/page.php b/tpl/page.php index 90c6385..0090630 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From 8f13dffd7c1b1b8c40cc0698390549e882ac46e2 Mon Sep 17 00:00:00 2001 From: Kyodev Date: Fri, 17 Mar 2017 22:23:01 +0100 Subject: [PATCH 41/59] fr translation completed --- i18n/fr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/fr.json b/i18n/fr.json index 97c4e24..61e0a3c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -149,12 +149,12 @@ "Editor": "Éditer", "Preview": "Prévisualiser", "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": - "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.", + "%s requiert que le PATH se termine dans un \"%s\". Veuillez mettre à jour le PATH dans votre index.php.", "Decrypt": - "Decrypt", + "Déchiffrer", "Enter password": "Entrez le mot de passe", - "Loading…": "Loading…", + "Loading…": "Chargement…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "In case this message never disappears please have a look at this FAQ for information to troubleshoot (in English)." + "Si ce message ne disparaîssait pas, jetez un oeil à cette FAQ pour des idées de résolution (en Anglais)." } From 21df49c7cd0d1c74cb670fa5fa96d38aeea65df0 Mon Sep 17 00:00:00 2001 From: Simone Esposito Date: Tue, 21 Mar 2017 20:44:46 +0100 Subject: [PATCH 42/59] README: Fix some grammar mistakes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0086f46..a259cb5 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Data is encrypted/decrypted in the browser using 256bit AES in [Galois Counter m This is a fork of ZeroBin, originally developed by [Sébastien Sauvage](https://github.com/sebsauvage/ZeroBin). It was refactored -to allow easier and cleaner extensions and has now much more features than the +to allow easier and cleaner extensions and has now many more features than the original. It is however still fully compatible to the original ZeroBin 0.19 data storage scheme. Therefore such installations can be upgraded to this fork -without loosing any data. +without losing any data. ## What PrivateBin provides From 037a312b8f1e5fef890d176cbb7a0ac9b3570df1 Mon Sep 17 00:00:00 2001 From: Stefano Martinelli Date: Wed, 22 Mar 2017 20:27:19 +0100 Subject: [PATCH 43/59] Italian translation update New message translated, couple of minor errors fixed and translation of row 70 reverted to a more faithful meaning. --- i18n/it.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/it.json b/i18n/it.json index df5aee2..06bfb23 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -67,7 +67,7 @@ "Never": "Mai", "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": - "Nota: questo è un servizio di prova, i dati possono essere cancellati in qualsiasi momento. Ti preghiamo di non abusare di questo servizio, grazie.", + "Nota: questo è un servizio di prova, i messaggi salvati possono essere cancellati in qualsiasi momento. Moriranno dei gattini se abuserai di questo servizio.", "This document will expire in %d seconds.": ["Questo documento scadrà tra un secondo.", "Questo documento scadrà in %d secondi."], "This document will expire in %d minutes.": @@ -87,13 +87,13 @@ "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "FOR YOUR EYES ONLY. Non chiudere questa finestra, il messaggio non può essere visualizzato una seconda volta.", "Could not decrypt comment; Wrong key?": - "Non riesco a decifrari il commento (Chiave errata?)", + "Non riesco a decifrare il commento (Chiave errata?)", "Reply": "Rispondi", "Anonymous": "Anonimo", "Anonymous avatar (Vizhash of the IP address)": - "Avatar Anonino (Vizhash dell'indirizzo IP)", + "Avatar Anonimo (Vizhash dell'indirizzo IP)", "Add comment": "Aggiungi un commento", "Optional nickname…": @@ -147,5 +147,5 @@ "Inserisci la password", "Loading…": "Loading…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "In case this message never disappears please have a look at this FAQ for information to troubleshoot (in English)." + "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema (in Inglese)." } From e58261b3f8f0e0bc2a7fe70ba48a1dfabfbeb523 Mon Sep 17 00:00:00 2001 From: R4SAS Date: Thu, 23 Mar 2017 18:36:08 +0300 Subject: [PATCH 44/59] update ru translation --- i18n/ru.json | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index 7e92da7..82ae2d1 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -7,8 +7,8 @@ "en": "ru", "Paste does not exist, has expired or has been deleted.": "Запись не существует, просрочена или была удалена.", - "%s requires php 5.3.0 or above to work. Sorry.": - "Для работы %s требуется PHP 5.3.0 или выше. Извините.", + "%s requires php 5.4.0 or above to work. Sorry.": + "Для работы %s требуется PHP 5.4.0 или выше. Извините.", "%s requires configuration section [%s] to be present in configuration file.": "%s необходимо наличие секции [%s] в конфигурационном файле.", "Please wait %d seconds between each post.": @@ -32,7 +32,7 @@ "Paste was properly deleted.": "Запись была успешно удалена.", "JavaScript is required for %s to work.
Sorry for the inconvenience.": - "Для работы %s требуется включенный JavaScript.
Приносим извинения за неудобства..", + "Для работы %s требуется включенный JavaScript.
Приносим извинения за неудобства.", "%s requires a modern browser to work.": "Для работы %s требуется более современный браузер.", "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": @@ -97,25 +97,25 @@ "Add comment": "Добавить комментарий", "Optional nickname…": - "Опциональный никнейм…", + "Опциональный никнейм...", "Post comment": "Отправить комментарий", "Sending comment…": - "Отправка комментария…", + "Отправка комментария...", "Comment posted.": "Комментарий опубликован.", - "Could not refresh display: %s": - "Невозможно обновить данные: %s", "unknown status": "неизвестная причина", "server error or not responding": "ошибка сервера или нет ответа", + "unknown error": + "неизвестная ошибка", "Could not post comment: %s": "Не удалось опубликовать комментарий: %s", - "Sending paste (Please move your mouse for more entropy)…": - "Отправка записи (Пожалуйста двигайте мышкой для большей энтропии)…", + "Please move your mouse for more entropy…": + "Пожалуйста двигайте мышкой для большей энтропии...", "Sending paste…": - "Отправка записи…", + "Отправка записи...", "Your paste is %s (Hit [Ctrl]+[c] to copy)": "Ссылка на запись %s (Нажмите [Ctrl]+[c] чтобы скопировать ссылку)", "Delete data": @@ -138,7 +138,10 @@ "Source Code": "Исходный код", "Markdown": "Язык разметки", "Download attachment": "Скачать прикрепленный файл", - "Cloned file attached.": "Дубль файла прикреплен.", + "Cloned file attached.": "Дубликат файла прикреплен.", + "Cloned: '%s'": "Дублировано: '%s'", + "Дубликат файла '%s' был прикреплен к этой записи.": + "Die geklonte Datei '%s' wurde angehängt.", "Attach a file": "Прикрепить файл", "Remove attachment": "Удалить вложение", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -155,5 +158,12 @@ "Enter password": "Введите пароль", "Uploading paste… Please wait.": - "Отправка записи… Пожалуйста подождите." + "Отправка записи... Пожалуйста подождите.", + "Loading…": "Загрузка...", + "Decrypting paste…": "Расшифровка записи...", + "Preparing new paste…": "Подготовка новой записи...", + "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": + "Если данное сообщение не исчезает длительное время, посмотрите этот FAQ с информацией о возможном решении проблемы.", + "+++ no paste text +++": + "+++ в записи нет текста +++" } From 39bb2fa389b4e95fdc1c2a97410849d05bfd6b8f Mon Sep 17 00:00:00 2001 From: Stefano Martinelli Date: Thu, 23 Mar 2017 17:36:13 +0100 Subject: [PATCH 45/59] php minimum version updated As per instructions, minimum version updated from 5.3.0 to 5.4.0 --- i18n/it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/it.json b/i18n/it.json index 06bfb23..30a80e9 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -7,8 +7,8 @@ "en": "it", "Paste does not exist, has expired or has been deleted.": "Questo messaggio non esiste, è scaduto o è stato cancellato.", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s richiede PHP 5.3.0 o superiore.", + "%s requires php 5.4.0 or above to work. Sorry.": + "%s richiede php 5.4.0 o superiore per funzionare. Ci spiace.", "%s requires configuration section [%s] to be present in configuration file.": "%s richiede la presenza della sezione [%s] nei file di configurazione.", "Please wait %d seconds between each post.": From 01701efd56569bd102cbc3f867a00acb99923feb Mon Sep 17 00:00:00 2001 From: R4SAS Date: Thu, 23 Mar 2017 21:20:52 +0300 Subject: [PATCH 46/59] changed three dots to unicode symbol, added note about english --- i18n/ru.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index 82ae2d1..707a41b 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -97,11 +97,11 @@ "Add comment": "Добавить комментарий", "Optional nickname…": - "Опциональный никнейм...", + "Опциональный никнейм…", "Post comment": "Отправить комментарий", "Sending comment…": - "Отправка комментария...", + "Отправка комментария…", "Comment posted.": "Комментарий опубликован.", "unknown status": @@ -113,9 +113,9 @@ "Could not post comment: %s": "Не удалось опубликовать комментарий: %s", "Please move your mouse for more entropy…": - "Пожалуйста двигайте мышкой для большей энтропии...", + "Пожалуйста двигайте мышкой для большей энтропии…", "Sending paste…": - "Отправка записи...", + "Отправка записи…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": "Ссылка на запись %s (Нажмите [Ctrl]+[c] чтобы скопировать ссылку)", "Delete data": @@ -158,12 +158,12 @@ "Enter password": "Введите пароль", "Uploading paste… Please wait.": - "Отправка записи... Пожалуйста подождите.", - "Loading…": "Загрузка...", - "Decrypting paste…": "Расшифровка записи...", - "Preparing new paste…": "Подготовка новой записи...", + "Отправка записи… Пожалуйста подождите.", + "Loading…": "Загрузка…", + "Decrypting paste…": "Расшифровка записи…", + "Preparing new paste…": "Подготовка новой записи…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Если данное сообщение не исчезает длительное время, посмотрите этот FAQ с информацией о возможном решении проблемы.", + "Если данное сообщение не исчезает длительное время, посмотрите этот FAQ с информацией о возможном решении проблемы (на английском).", "+++ no paste text +++": "+++ в записи нет текста +++" } From 9a788d63ee999706d42a3ca6a330e596552b9c6c Mon Sep 17 00:00:00 2001 From: R4SAS Date: Thu, 23 Mar 2017 21:25:44 +0300 Subject: [PATCH 47/59] fixed wrong translated line --- i18n/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index 707a41b..ae60abb 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -140,8 +140,8 @@ "Download attachment": "Скачать прикрепленный файл", "Cloned file attached.": "Дубликат файла прикреплен.", "Cloned: '%s'": "Дублировано: '%s'", - "Дубликат файла '%s' был прикреплен к этой записи.": - "Die geklonte Datei '%s' wurde angehängt.", + "The cloned file '%s' was attached to this paste.": + "Дубликат файла '%s' был прикреплен к этой записи.", "Attach a file": "Прикрепить файл", "Remove attachment": "Удалить вложение", "Your browser does not support uploading encrypted files. Please use a newer browser.": From 02e0b8655d622394cbd1398fa43b5fc3d78516ba Mon Sep 17 00:00:00 2001 From: TMs Date: Fri, 24 Mar 2017 11:32:12 +0100 Subject: [PATCH 48/59] update zh translation --- i18n/zh.json | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/i18n/zh.json b/i18n/zh.json index 41efcc4..d779c04 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -7,8 +7,8 @@ "en": "zh", "Paste does not exist, has expired or has been deleted.": "粘贴不存在,已过期或者已被删除。", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s需要工作于PHP 5.3.0及以上版本,抱歉。", + "%s requires php 5.4.0 or above to work. Sorry.": + "%s需要工作于PHP 5.4.0及以上版本,抱歉。", "%s requires configuration section [%s] to be present in configuration file.": "%s需要设置配置文件中 [%s] 的部分。", "Please wait %d seconds between each post.": @@ -92,8 +92,8 @@ "回复", "Anonymous": "匿名", - "Anonymous avatar (Vizhash of the IP address)": - "匿名头像 (由IP地址生成Vizhash)", + "Avatar generated from IP address": + "由IP生成的头像", "Add comment": "添加评论", "Optional nickname…": @@ -112,8 +112,8 @@ "服务器错误或无回应", "Could not post comment: %s": "无法发送评论: %s", - "Sending paste (Please move your mouse for more entropy)…": - "粘贴提交中 (请移动鼠标以产生更多熵)…", + "Please move your mouse for more entropy…": + "请移动鼠标增加随机性…", "Sending paste…": "粘贴提交中…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,8 @@ "Source Code": "源代码", "Markdown": "Markdown", "Download attachment": "下载附件", - "Cloned file attached.": "已附加克隆的文件", + "Cloned: '%s'": "克隆: '%s'", + "The cloned file '%s' was attached to this paste.": "克隆文件 '%s' 已附加到此粘贴。", "Attach a file": "添加一个附件", "Remove attachment": "移除附件", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -146,6 +147,9 @@ "Enter password": "输入密码", "Loading…": "载入中…", + "Decrypting paste…": "正在解密", + "Preparing new paste…": "正在准备新的粘贴", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "如果这个消息一直不消失,请参考 这里的 FAQ 进行故障排除 (英文版)。" + "如果这个消息一直不消失,请参考 这里的 FAQ 进行故障排除 (英文版)。", + "+++ no paste text +++": "+++ 没有粘贴内容 +++" } From a6b9bc68798a7f03c2410c59749f914507673d41 Mon Sep 17 00:00:00 2001 From: R4SAS Date: Fri, 24 Mar 2017 17:50:53 +0300 Subject: [PATCH 49/59] replace php version to constant, #186 #201 --- i18n/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index ae60abb..ca4c707 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -7,8 +7,8 @@ "en": "ru", "Paste does not exist, has expired or has been deleted.": "Запись не существует, просрочена или была удалена.", - "%s requires php 5.4.0 or above to work. Sorry.": - "Для работы %s требуется PHP 5.4.0 или выше. Извините.", + "%s requires php %s or above to work. Sorry.": + "Для работы %s требуется php %s или выше. Извините.", "%s requires configuration section [%s] to be present in configuration file.": "%s необходимо наличие секции [%s] в конфигурационном файле.", "Please wait %d seconds between each post.": From 88b02d866e779b9b88c22a71554723aee50115e7 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 19:20:34 +0100 Subject: [PATCH 50/59] fixes #186 for good --- INSTALL.md | 2 +- composer.json | 2 +- i18n/de.json | 4 ++-- i18n/es.json | 4 ++-- i18n/fr.json | 4 ++-- i18n/it.json | 4 ++-- i18n/no.json | 4 ++-- i18n/oc.json | 4 ++-- i18n/pl.json | 4 ++-- i18n/pt.json | 4 ++-- i18n/sl.json | 4 ++-- i18n/zh.json | 4 ++-- lib/PrivateBin.php | 11 +++++++++-- 13 files changed, 31 insertions(+), 24 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 5dbc509..b627bc9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ check the options and adjust them as you see fit. ### Requirements -- PHP version 5.3 or above +- PHP version 5.4 or above - _one_ of the following sources of cryptographically safe randomness is required: - PHP 7 or higher - [Libsodium](https://download.libsodium.org/libsodium/content/installation/) and it's [PHP extension](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium) diff --git a/composer.json b/composer.json index 632bf2b..4248b5f 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } ], "require": { - "php": "^5.3.0 || ^7.0", + "php": "^5.4.0 || ^7.0", "paragonie/random_compat": "2.0.4", "yzalis/identicon": "1.1.0" }, diff --git a/i18n/de.json b/i18n/de.json index 9959e71..7974d77 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -7,8 +7,8 @@ "en": "de", "Paste does not exist, has expired or has been deleted.": "Diesen Text gibt es nicht, er ist abgelaufen oder wurde gelöscht.", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s benötigt PHP 5.3.0 oder höher, um zu funktionieren. Sorry.", + "%s requires php %s or above to work. Sorry.": + "%s benötigt PHP %s oder höher, um zu funktionieren. Sorry.", "%s requires configuration section [%s] to be present in configuration file.": "%s benötigt den Konfigurationsabschnitt [%s] in der Konfigurationsdatei um zu funktionieren.", "Please wait %d seconds between each post.": diff --git a/i18n/es.json b/i18n/es.json index 1e2fd48..e957c8f 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -7,8 +7,8 @@ "en": "es", "Paste does not exist, has expired or has been deleted.": "El texto no existe, ha caducado o ha sido eliminado.", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s requiere php 5.3.0 o superior para funcionar. Lo siento.", + "%s requires php %s or above to work. Sorry.": + "%s requiere php %s o superior para funcionar. Lo siento.", "%s requires configuration section [%s] to be present in configuration file.": "%s requiere que la sección de configuración [%s] esté presente en el archivo de configuración.", "Please wait %d seconds between each post.": diff --git a/i18n/fr.json b/i18n/fr.json index 61e0a3c..35cfd65 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -7,8 +7,8 @@ "en": "fr", "Paste does not exist, has expired or has been deleted.": "Le paste n'existe pas, a expiré, ou a été supprimé.", - "%s requires php 5.3.0 or above to work. Sorry.": - "Désolé, %s nécessite php 5.3.0 ou supérieur pour fonctionner.", + "%s requires php %s or above to work. Sorry.": + "Désolé, %s nécessite php %s ou supérieur pour fonctionner.", "%s requires configuration section [%s] to be present in configuration file.": "%s a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.", "Please wait %d seconds between each post.": diff --git a/i18n/it.json b/i18n/it.json index 30a80e9..dc56fa7 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -7,8 +7,8 @@ "en": "it", "Paste does not exist, has expired or has been deleted.": "Questo messaggio non esiste, è scaduto o è stato cancellato.", - "%s requires php 5.4.0 or above to work. Sorry.": - "%s richiede php 5.4.0 o superiore per funzionare. Ci spiace.", + "%s requires php %s or above to work. Sorry.": + "%s richiede php %s o superiore per funzionare. Ci spiace.", "%s requires configuration section [%s] to be present in configuration file.": "%s richiede la presenza della sezione [%s] nei file di configurazione.", "Please wait %d seconds between each post.": diff --git a/i18n/no.json b/i18n/no.json index 4d92cc8..fd56583 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -7,8 +7,8 @@ "en": "no", "Paste does not exist, has expired or has been deleted.": "Innlegget eksisterer ikke, er utløpt eller har blitt slettet.", - "%s requires php 5.3.0 or above to work. Sorry.": - "Beklager, %s krever php 5.3.0 eller nyere for å kjøre.", + "%s requires php %s or above to work. Sorry.": + "Beklager, %s krever php %s eller nyere for å kjøre.", "%s requires configuration section [%s] to be present in configuration file.": "%s krever konfigurasjonsdel [%s] å være til stede i konfigurasjonsfilen .", "Please wait %d seconds between each post.": diff --git a/i18n/oc.json b/i18n/oc.json index a29bce0..b6b449d 100644 --- a/i18n/oc.json +++ b/i18n/oc.json @@ -7,8 +7,8 @@ "en": "oc", "Paste does not exist, has expired or has been deleted.": "Lo tèxte existís pas, a expirat, o es estat suprimit.", - "%s requires php 5.3.0 or above to work. Sorry.": - "O planhèm, %s necessita php 5.3.0 o superior per foncionar.", + "%s requires php %s or above to work. Sorry.": + "O planhèm, %s necessita php %s o superior per foncionar.", "%s requires configuration section [%s] to be present in configuration file.": "%s fa besonh de la seccion de configuracion [%s] dins lo fichièr de configuracion per foncionar.", "Please wait %d seconds between each post.": diff --git a/i18n/pl.json b/i18n/pl.json index b9cc8f2..9955498 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -7,8 +7,8 @@ "en": "pl", "Paste does not exist, has expired or has been deleted.": "Wklejka nie istnieje, wygasła albo została usunięta.", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s wymaga PHP w wersji 5.3.0 lub nowszej, sorry.", + "%s requires php %s or above to work. Sorry.": + "%s wymaga PHP w wersji %s lub nowszej, sorry.", "%s requires configuration section [%s] to be present in configuration file.": "%s wymaga obecności sekcji [%s] w pliku konfiguracyjnym.", "Please wait %d seconds between each post.": diff --git a/i18n/pt.json b/i18n/pt.json index e00a4a1..ab45952 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -7,8 +7,8 @@ "en": "pt", "Paste does not exist, has expired or has been deleted.": "A cópia não existe, expirou ou já foi excluída.", - "%s requires php 5.3.0 or above to work. Sorry.": - "%s requer php 5.3.0 ou superior para funcionar. Desculpa.", + "%s requires php %s or above to work. Sorry.": + "%s requer php %s ou superior para funcionar. Desculpa.", "%s requires configuration section [%s] to be present in configuration file.": "%s requer que a seção de configuração [% s] esteja no arquivo de configuração.", "Please wait %d seconds between each post.": diff --git a/i18n/sl.json b/i18n/sl.json index 2df2608..00b0970 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -7,8 +7,8 @@ "en": "sl", "Paste does not exist, has expired or has been deleted.": "Prilepek ne obstaja, mu je potekla življenjska doba, ali pa je izbrisan.", - "%s requires php 5.3.0 or above to work. Sorry.": - "Oprosti, %s za delovanje potrebuje vsaj php 5.3.0.", + "%s requires php %s or above to work. Sorry.": + "Oprosti, %s za delovanje potrebuje vsaj php %s.", "%s requires configuration section [%s] to be present in configuration file.": "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.", "Please wait %d seconds between each post.": diff --git a/i18n/zh.json b/i18n/zh.json index d779c04..5fcaf3d 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -7,8 +7,8 @@ "en": "zh", "Paste does not exist, has expired or has been deleted.": "粘贴不存在,已过期或者已被删除。", - "%s requires php 5.4.0 or above to work. Sorry.": - "%s需要工作于PHP 5.4.0及以上版本,抱歉。", + "%s requires php %s or above to work. Sorry.": + "%s需要工作于PHP %s及以上版本,抱歉。", "%s requires configuration section [%s] to be present in configuration file.": "%s需要设置配置文件中 [%s] 的部分。", "Please wait %d seconds between each post.": diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index fb3e523..e874015 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -30,6 +30,13 @@ class PrivateBin */ const VERSION = '1.1'; + /** + * minimal required PHP version + * + * @const string + */ + const MIN_PHP_VERSION = '5.4.0'; + /** * show the same error message if the paste expired or does not exist * @@ -120,8 +127,8 @@ class PrivateBin */ public function __construct() { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - throw new Exception(I18n::_('%s requires php 5.4.0 or above to work. Sorry.', I18n::_('PrivateBin')), 1); + if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) { + throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1); } if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) { throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5); From 850b28931f1c3edbda41dce0c078fcf4b554d83f Mon Sep 17 00:00:00 2001 From: Kyodev Date: Fri, 24 Mar 2017 19:56:52 +0100 Subject: [PATCH 51/59] php minimum version updated --- i18n/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/fr.json b/i18n/fr.json index 61e0a3c..c889eaf 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -7,8 +7,8 @@ "en": "fr", "Paste does not exist, has expired or has been deleted.": "Le paste n'existe pas, a expiré, ou a été supprimé.", - "%s requires php 5.3.0 or above to work. Sorry.": - "Désolé, %s nécessite php 5.3.0 ou supérieur pour fonctionner.", + "%s requires php 5.4.0 or above to work. Sorry.": + "Désolé, %s nécessite php 5.4.0 ou supérieur pour fonctionner.", "%s requires configuration section [%s] to be present in configuration file.": "%s a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.", "Please wait %d seconds between each post.": From 1cb1c1ced8d7ea68e2e49fc825d4673d625615c3 Mon Sep 17 00:00:00 2001 From: Kyodev Date: Fri, 24 Mar 2017 20:34:35 +0100 Subject: [PATCH 52/59] php minimum version + constant version --- i18n/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/fr.json b/i18n/fr.json index c889eaf..35cfd65 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -7,8 +7,8 @@ "en": "fr", "Paste does not exist, has expired or has been deleted.": "Le paste n'existe pas, a expiré, ou a été supprimé.", - "%s requires php 5.4.0 or above to work. Sorry.": - "Désolé, %s nécessite php 5.4.0 ou supérieur pour fonctionner.", + "%s requires php %s or above to work. Sorry.": + "Désolé, %s nécessite php %s ou supérieur pour fonctionner.", "%s requires configuration section [%s] to be present in configuration file.": "%s a besoin de la section de configuration [%s] dans le fichier de configuration pour fonctionner.", "Please wait %d seconds between each post.": From ce92bfa934fd4df5d517216f31fe8c6aa97d51ff Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 21:30:08 +0100 Subject: [PATCH 53/59] updated .htaccess format, refactored .htaccess creation logic and improving code coverage, fixes #194 --- .gitignore | 1 + lib/Data/Filesystem.php | 30 ++++++++------ lib/Persistence/AbstractPersistence.php | 9 ++--- lib/PrivateBin.php | 11 ----- tst/.gitignore | 1 - tst/.htaccess | 2 - tst/Data/FilesystemTest.php | 33 +++++++++++++++ tst/ModelTest.php | 53 +++++++++++++++++++++++++ tst/PrivateBinTest.php | 21 +++++----- tst/SjclTest.php | 2 + 10 files changed, 119 insertions(+), 44 deletions(-) delete mode 100644 tst/.gitignore delete mode 100644 tst/.htaccess diff --git a/.gitignore b/.gitignore index 9f09f53..a752f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ vendor/**/build_phar.php # Ignore local node modules, unit testing logs, api docs and eclipse project files js/node_modules/ tst/log/ +tst/ConfigurationCombinationsTest.php .settings .buildpath .project diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index ca9befb..393d3d8 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -12,6 +12,7 @@ namespace PrivateBin\Data; +use Exception; use PrivateBin\Json; use PrivateBin\Model\Paste; @@ -41,16 +42,16 @@ class Filesystem extends AbstractData */ public static function getInstance($options = null) { + // if needed initialize the singleton + if (!(self::$_instance instanceof self)) { + self::$_instance = new self; + } // if given update the data directory if ( is_array($options) && array_key_exists('dir', $options) ) { self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR; - } - // if needed initialize the singleton - if (!(self::$_instance instanceof self)) { - self::$_instance = new self; self::_init(); } return self::$_instance; @@ -293,7 +294,7 @@ class Filesystem extends AbstractData } /** - * initialize privatebin + * Initialize data store * * @access private * @static @@ -303,15 +304,20 @@ class Filesystem extends AbstractData { // Create storage directory if it does not exist. if (!is_dir(self::$_dir)) { - mkdir(self::$_dir, 0700); + if (!@mkdir(self::$_dir, 0700)) { + throw new Exception('unable to create directory ' . self::$_dir, 10); + } } - // Create .htaccess file if it does not exist. - if (!is_file(self::$_dir . '.htaccess')) { - file_put_contents( - self::$_dir . '.htaccess', - 'Allow from none' . PHP_EOL . - 'Deny from all' . PHP_EOL + $file = self::$_dir . DIRECTORY_SEPARATOR . '.htaccess'; + if (!is_file($file)) { + $writtenBytes = @file_put_contents( + $file, + 'Require all denied' . PHP_EOL, + LOCK_EX ); + if ($writtenBytes === false || $writtenBytes < 19) { + throw new Exception('unable to write to file ' . $file, 11); + } } } diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 9aaa70b..68f148f 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -86,21 +86,18 @@ abstract class AbstractPersistence { // Create storage directory if it does not exist. if (!is_dir(self::$_path)) { - if (!@mkdir(self::$_path)) { + if (!@mkdir(self::$_path, 0700)) { throw new Exception('unable to create directory ' . self::$_path, 10); } } - - // Create .htaccess file if it does not exist. $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($file)) { $writtenBytes = @file_put_contents( $file, - 'Allow from none' . PHP_EOL . - 'Deny from all' . PHP_EOL, + 'Require all denied' . PHP_EOL, LOCK_EX ); - if ($writtenBytes === false || $writtenBytes < 30) { + if ($writtenBytes === false || $writtenBytes < 19) { throw new Exception('unable to write to file ' . $file, 11); } } diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index e874015..92072ea 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -175,17 +175,6 @@ class PrivateBin */ private function _init() { - foreach (array('cfg', 'lib') as $dir) { - if (!is_file(PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess')) { - file_put_contents( - PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess', - 'Allow from none' . PHP_EOL . - 'Deny from all' . PHP_EOL, - LOCK_EX - ); - } - } - $this->_conf = new Configuration; $this->_model = new Model($this->_conf); $this->_request = new Request; diff --git a/tst/.gitignore b/tst/.gitignore deleted file mode 100644 index 39ef6b9..0000000 --- a/tst/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ConfigurationCombinationsTest.php diff --git a/tst/.htaccess b/tst/.htaccess deleted file mode 100644 index b584d98..0000000 --- a/tst/.htaccess +++ /dev/null @@ -1,2 +0,0 @@ -Allow from none -Deny from all diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php index 9502921..7cf5ee8 100644 --- a/tst/Data/FilesystemTest.php +++ b/tst/Data/FilesystemTest.php @@ -8,16 +8,26 @@ class FilesystemTest extends PHPUnit_Framework_TestCase private $_path; + private $_invalidPath; + public function setUp() { /* Setup Routine */ $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; + $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); + if (!is_dir($this->_path)) { + mkdir($this->_path); + } + if (!is_dir($this->_invalidPath)) { + mkdir($this->_invalidPath); + } } public function tearDown() { /* Tear Down Routine */ + chmod($this->_invalidPath, 0700); Helper::rmDir($this->_path); } @@ -37,6 +47,7 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist'); $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'store comment'); $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it'); + $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'unable to store the same comment twice'); $comment = json_decode(json_encode(Helper::getComment())); $comment->id = Helper::getCommentId(); $comment->parentid = Helper::getPasteId(); @@ -127,4 +138,26 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment'); $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist'); } + + /** + * @expectedException Exception + * @expectedExceptionCode 10 + */ + public function testPermissionShenanigans() + { + // try creating an invalid path + chmod($this->_invalidPath, 0000); + Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz')); + } + + /** + * @expectedException Exception + * @expectedExceptionCode 11 + */ + public function testPathShenanigans() + { + // try setting an invalid path + chmod($this->_invalidPath, 0000); + Filesystem::getInstance(array('dir' => $this->_invalidPath)); + } } diff --git a/tst/ModelTest.php b/tst/ModelTest.php index 8f7a40b..ac4e92f 100644 --- a/tst/ModelTest.php +++ b/tst/ModelTest.php @@ -82,6 +82,7 @@ class ModelTest extends PHPUnit_Framework_TestCase $comment = $paste->getComment(Helper::getPasteId()); $comment->setData($commentData['data']); $comment->setNickname($commentData['meta']['nickname']); + $comment->getParentId(); $comment->store(); $comment = $paste->getComment(Helper::getPasteId(), Helper::getCommentId()); @@ -189,6 +190,27 @@ class ModelTest extends PHPUnit_Framework_TestCase $this->assertFalse(Paste::isValidId('../bar/baz'), 'path attack'); } + /** + * @expectedException Exception + * @expectedExceptionCode 64 + */ + public function testInvalidPaste() + { + $this->_model->getPaste(Helper::getPasteId())->delete(); + $paste = $this->_model->getPaste(Helper::getPasteId()); + $paste->get(); + } + + /** + * @expectedException Exception + * @expectedExceptionCode 61 + */ + public function testInvalidData() + { + $paste = $this->_model->getPaste(); + $paste->setData(''); + } + /** * @expectedException Exception * @expectedExceptionCode 62 @@ -199,6 +221,37 @@ class ModelTest extends PHPUnit_Framework_TestCase $paste->getComment(Helper::getPasteId()); } + /** + * @expectedException Exception + * @expectedExceptionCode 67 + */ + public function testInvalidCommentDeletedPaste() + { + $pasteData = Helper::getPaste(); + $paste = $this->_model->getPaste(Helper::getPasteId()); + $paste->setData($pasteData['data']); + $paste->store(); + + $comment = $paste->getComment(Helper::getPasteId()); + $paste->delete(); + $comment->store(); + } + + /** + * @expectedException Exception + * @expectedExceptionCode 68 + */ + public function testInvalidCommentData() + { + $pasteData = Helper::getPaste(); + $paste = $this->_model->getPaste(Helper::getPasteId()); + $paste->setData($pasteData['data']); + $paste->store(); + + $comment = $paste->getComment(Helper::getPasteId()); + $comment->store(); + } + public function testExpiration() { $pasteData = Helper::getPaste(); diff --git a/tst/PrivateBinTest.php b/tst/PrivateBinTest.php index fbf5b60..a8aad11 100644 --- a/tst/PrivateBinTest.php +++ b/tst/PrivateBinTest.php @@ -140,21 +140,18 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase public function testHtaccess() { $this->reset(); - $dirs = array('cfg', 'lib'); - foreach ($dirs as $dir) { - $file = PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess'; - @unlink($file); - } + $file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess'; + @unlink($file); + + $_POST = Helper::getPaste(); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REMOTE_ADDR'] = '::1'; ob_start(); new PrivateBin; ob_end_clean(); - foreach ($dirs as $dir) { - $file = PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess'; - $this->assertFileExists( - $file, - "$dir htaccess recreated" - ); - } + + $this->assertFileExists($file, 'htaccess recreated'); } /** diff --git a/tst/SjclTest.php b/tst/SjclTest.php index 54cc30f..a9d947e 100644 --- a/tst/SjclTest.php +++ b/tst/SjclTest.php @@ -1,11 +1,13 @@ assertTrue(Sjcl::isValid($paste['data']), 'valid sjcl'); $this->assertTrue(Sjcl::isValid($paste['attachment']), 'valid sjcl'); From 6db9dae66b405e841a71eb1ffd4405fdf66befbe Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 21:35:50 +0100 Subject: [PATCH 54/59] applying styleCI recommendations --- tst/Data/FilesystemTest.php | 4 ++-- tst/ModelTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php index 7cf5ee8..fe012c4 100644 --- a/tst/Data/FilesystemTest.php +++ b/tst/Data/FilesystemTest.php @@ -13,9 +13,9 @@ class FilesystemTest extends PHPUnit_Framework_TestCase public function setUp() { /* Setup Routine */ - $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; + $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar'; - $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); + $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); if (!is_dir($this->_path)) { mkdir($this->_path); } diff --git a/tst/ModelTest.php b/tst/ModelTest.php index ac4e92f..4d314f7 100644 --- a/tst/ModelTest.php +++ b/tst/ModelTest.php @@ -228,7 +228,7 @@ class ModelTest extends PHPUnit_Framework_TestCase public function testInvalidCommentDeletedPaste() { $pasteData = Helper::getPaste(); - $paste = $this->_model->getPaste(Helper::getPasteId()); + $paste = $this->_model->getPaste(Helper::getPasteId()); $paste->setData($pasteData['data']); $paste->store(); @@ -244,7 +244,7 @@ class ModelTest extends PHPUnit_Framework_TestCase public function testInvalidCommentData() { $pasteData = Helper::getPaste(); - $paste = $this->_model->getPaste(Helper::getPasteId()); + $paste = $this->_model->getPaste(Helper::getPasteId()); $paste->setData($pasteData['data']); $paste->store(); From f7853cf439df613838d7c3fe710f423956f943a2 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 23:42:11 +0100 Subject: [PATCH 55/59] removing duplicate code, cleanup of temporary test files --- lib/Data/Filesystem.php | 74 +++++++++-------------------------- lib/Persistence/DataStore.php | 48 +++++++++++++++++++++++ tst/Bootstrap.php | 28 +++++++------ tst/Data/FilesystemTest.php | 30 -------------- tst/JsonApiTest.php | 2 + tst/RequestTest.php | 1 + 6 files changed, 85 insertions(+), 98 deletions(-) create mode 100644 lib/Persistence/DataStore.php diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index 393d3d8..0984405 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -12,9 +12,8 @@ namespace PrivateBin\Data; -use Exception; -use PrivateBin\Json; use PrivateBin\Model\Paste; +use PrivateBin\Persistence\DataStore; /** * Filesystem @@ -23,15 +22,6 @@ use PrivateBin\Model\Paste; */ class Filesystem extends AbstractData { - /** - * directory where data is stored - * - * @access private - * @static - * @var string - */ - private static $_dir = 'data/'; - /** * get instance of singleton * @@ -51,8 +41,7 @@ class Filesystem extends AbstractData is_array($options) && array_key_exists('dir', $options) ) { - self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR; - self::_init(); + DataStore::setPath($options['dir']); } return self::$_instance; } @@ -63,19 +52,19 @@ class Filesystem extends AbstractData * @access public * @param string $pasteid * @param array $paste - * @throws Exception * @return bool */ public function create($pasteid, $paste) { $storagedir = self::_dataid2path($pasteid); - if (is_file($storagedir . $pasteid)) { + $file = $storagedir . $pasteid; + if (is_file($file)) { return false; } if (!is_dir($storagedir)) { mkdir($storagedir, 0700, true); } - return (bool) file_put_contents($storagedir . $pasteid, Json::encode($paste)); + return DataStore::store($file, $paste); } /** @@ -156,20 +145,19 @@ class Filesystem extends AbstractData * @param string $parentid * @param string $commentid * @param array $comment - * @throws Exception * @return bool */ public function createComment($pasteid, $parentid, $commentid, $comment) { $storagedir = self::_dataid2discussionpath($pasteid); - $filename = $pasteid . '.' . $commentid . '.' . $parentid; - if (is_file($storagedir . $filename)) { + $file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid; + if (is_file($file)) { return false; } if (!is_dir($storagedir)) { mkdir($storagedir, 0700, true); } - return (bool) file_put_contents($storagedir . $filename, Json::encode($comment)); + return DataStore::store($file, $comment); } /** @@ -238,8 +226,9 @@ class Filesystem extends AbstractData protected function _getExpiredPastes($batchsize) { $pastes = array(); + $mainpath = DataStore::getPath(); $firstLevel = array_filter( - scandir(self::$_dir), + scandir($mainpath), 'self::_isFirstLevelDir' ); if (count($firstLevel) > 0) { @@ -247,7 +236,7 @@ class Filesystem extends AbstractData for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) { $firstKey = array_rand($firstLevel); $secondLevel = array_filter( - scandir(self::$_dir . $firstLevel[$firstKey]), + scandir($mainpath . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]), 'self::_isSecondLevelDir' ); @@ -258,8 +247,9 @@ class Filesystem extends AbstractData } $secondKey = array_rand($secondLevel); - $path = self::$_dir . $firstLevel[$firstKey] . - DIRECTORY_SEPARATOR . $secondLevel[$secondKey]; + $path = $mainpath . DIRECTORY_SEPARATOR . + $firstLevel[$firstKey] . DIRECTORY_SEPARATOR . + $secondLevel[$secondKey]; if (!is_dir($path)) { continue; } @@ -293,34 +283,6 @@ class Filesystem extends AbstractData return $pastes; } - /** - * Initialize data store - * - * @access private - * @static - * @return void - */ - private static function _init() - { - // Create storage directory if it does not exist. - if (!is_dir(self::$_dir)) { - if (!@mkdir(self::$_dir, 0700)) { - throw new Exception('unable to create directory ' . self::$_dir, 10); - } - } - $file = self::$_dir . DIRECTORY_SEPARATOR . '.htaccess'; - if (!is_file($file)) { - $writtenBytes = @file_put_contents( - $file, - 'Require all denied' . PHP_EOL, - LOCK_EX - ); - if ($writtenBytes === false || $writtenBytes < 19) { - throw new Exception('unable to write to file ' . $file, 11); - } - } - } - /** * Convert paste id to storage path. * @@ -338,8 +300,10 @@ class Filesystem extends AbstractData */ private static function _dataid2path($dataid) { - return self::$_dir . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR . - substr($dataid, 2, 2) . DIRECTORY_SEPARATOR; + return DataStore::getPath( + substr($dataid, 0, 2) . DIRECTORY_SEPARATOR . + substr($dataid, 2, 2) . DIRECTORY_SEPARATOR + ); } /** @@ -369,7 +333,7 @@ class Filesystem extends AbstractData private static function _isFirstLevelDir($element) { return self::_isSecondLevelDir($element) && - is_dir(self::$_dir . DIRECTORY_SEPARATOR . $element); + is_dir(DataStore::getPath($element)); } /** diff --git a/lib/Persistence/DataStore.php b/lib/Persistence/DataStore.php new file mode 100644 index 0000000..27c7131 --- /dev/null +++ b/lib/Persistence/DataStore.php @@ -0,0 +1,48 @@ +read())) { - if ($file != '.' && $file != '..') { - if (is_dir($path . $file)) { - self::rmDir($path . $file); - } elseif (is_file($path . $file)) { - if (!unlink($path . $file)) { - throw new Exception('Error deleting file "' . $path . $file . '".'); + if (is_dir($path)) { + $path .= DIRECTORY_SEPARATOR; + $dir = dir($path); + while (false !== ($file = $dir->read())) { + if ($file != '.' && $file != '..') { + if (is_dir($path . $file)) { + self::rmDir($path . $file); + } elseif (is_file($path . $file)) { + if (!unlink($path . $file)) { + throw new Exception('Error deleting file "' . $path . $file . '".'); + } } } } - } - $dir->close(); - if (!rmdir($path)) { - throw new Exception('Error deleting directory "' . $path . '".'); + $dir->close(); + if (!rmdir($path)) { + throw new Exception('Error deleting directory "' . $path . '".'); + } } } diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php index fe012c4..e7e6dc8 100644 --- a/tst/Data/FilesystemTest.php +++ b/tst/Data/FilesystemTest.php @@ -110,10 +110,6 @@ class FilesystemTest extends PHPUnit_Framework_TestCase } } - /** - * @expectedException Exception - * @expectedExceptionCode 90 - */ public function testErrorDetection() { $this->_model->delete(Helper::getPasteId()); @@ -123,10 +119,6 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist'); } - /** - * @expectedException Exception - * @expectedExceptionCode 90 - */ public function testCommentErrorDetection() { $this->_model->delete(Helper::getPasteId()); @@ -138,26 +130,4 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment'); $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist'); } - - /** - * @expectedException Exception - * @expectedExceptionCode 10 - */ - public function testPermissionShenanigans() - { - // try creating an invalid path - chmod($this->_invalidPath, 0000); - Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz')); - } - - /** - * @expectedException Exception - * @expectedExceptionCode 11 - */ - public function testPathShenanigans() - { - // try setting an invalid path - chmod($this->_invalidPath, 0000); - Filesystem::getInstance(array('dir' => $this->_invalidPath)); - } } diff --git a/tst/JsonApiTest.php b/tst/JsonApiTest.php index 5cf1360..a592889 100644 --- a/tst/JsonApiTest.php +++ b/tst/JsonApiTest.php @@ -98,6 +98,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase new PrivateBin; $content = ob_get_contents(); ob_end_clean(); + unlink($file); $response = json_decode($content, true); $this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputted paste ID matches input'); @@ -132,6 +133,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase new PrivateBin; $content = ob_get_contents(); ob_end_clean(); + unlink($file); $response = json_decode($content, true); $this->assertEquals(0, $response['status'], 'outputs status'); $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted'); diff --git a/tst/RequestTest.php b/tst/RequestTest.php index f20209f..29b0dad 100644 --- a/tst/RequestTest.php +++ b/tst/RequestTest.php @@ -63,6 +63,7 @@ class RequestTest extends PHPUnit_Framework_TestCase file_put_contents($file, 'data=foo'); Request::setInputStream($file); $request = new Request; + unlink($file); $this->assertTrue($request->isJsonApiCall(), 'is JSON Api call'); $this->assertEquals('create', $request->getOperation()); $this->assertEquals('foo', $request->getParam('data')); From 18315e7de0f3fbebf2e5c03abda8c24fb5563004 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 23:45:10 +0100 Subject: [PATCH 56/59] removing unused class --- lib/Persistence/DataStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Persistence/DataStore.php b/lib/Persistence/DataStore.php index 27c7131..26c8ceb 100644 --- a/lib/Persistence/DataStore.php +++ b/lib/Persistence/DataStore.php @@ -13,7 +13,6 @@ namespace PrivateBin\Persistence; use Exception; -use PrivateBin\Configuration; use PrivateBin\Json; /** From 9b2af0abf567ee33cb1205ef84f2a698b2c9b990 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 24 Mar 2017 23:54:37 +0100 Subject: [PATCH 57/59] fixing documentation --- lib/Persistence/DataStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Persistence/DataStore.php b/lib/Persistence/DataStore.php index 26c8ceb..56dde1a 100644 --- a/lib/Persistence/DataStore.php +++ b/lib/Persistence/DataStore.php @@ -28,7 +28,7 @@ class DataStore extends AbstractPersistence * @access public * @static * @param string $filename - * @param string $data + * @param array $data * @return bool */ public static function store($filename, $data) From 3be736fa1daa1a2b4d6d429612b1c6599f689041 Mon Sep 17 00:00:00 2001 From: Tulio Leao Date: Fri, 24 Mar 2017 20:03:08 -0300 Subject: [PATCH 58/59] Update pt.json to reflect latest string changes Just a minimal change on the translation. --- i18n/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/pt.json b/i18n/pt.json index ab45952..06f72c5 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -93,7 +93,7 @@ "Anonymous": "Anônimo", "Avatar generated from IP address": - "Avatar (do endereço IP)", + "Avatar gerado à partir do endereço IP", "Add comment": "Adicionar comentário", "Optional nickname…": From bbcc3e167bb52065cd80ddc7becb6a00eae63460 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 25 Mar 2017 00:58:59 +0100 Subject: [PATCH 59/59] implementing recommendations of scrutinizer --- js/privatebin.js | 60 ++++++++++--------------- lib/Data/AbstractData.php | 4 +- lib/Data/Database.php | 14 ++---- lib/Data/Filesystem.php | 1 - lib/I18n.php | 9 ++-- lib/Model.php | 3 -- lib/Model/AbstractModel.php | 5 --- lib/Model/Comment.php | 5 --- lib/Model/Paste.php | 8 ---- lib/Persistence/AbstractPersistence.php | 2 - lib/Persistence/PurgeLimiter.php | 2 - lib/Persistence/ServerSalt.php | 1 - lib/Persistence/TrafficLimiter.php | 2 - lib/PrivateBin.php | 7 --- lib/Request.php | 21 +++++---- lib/View.php | 2 - lib/Vizhash16x16.php | 2 - tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 19 files changed, 45 insertions(+), 107 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 10b6a3a..9fd4626 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -179,7 +179,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name Helper.urls2links * @function - * @param {Object} element - a jQuery DOM element + * @param {Object} $element - a jQuery DOM element */ me.urls2links = function($element) { @@ -675,7 +675,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @return {string} func */ - me.getSymmetricKey = function(func) + me.getSymmetricKey = function() { return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0); } @@ -903,8 +903,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.isVisible = function($element) { var elementTop = $element.offset().top; - var elementBottom = elementTop + $element.outerHeight(); - var viewportTop = $(window).scrollTop(); var viewportBottom = viewportTop + $(window).height(); @@ -985,11 +983,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * Alert/error manager * * @name Alert - * @param {object} window - * @param {object} document * @class */ - var Alert = (function (window, document) { + var Alert = (function () { var me = {}; var $errorMessage, @@ -1249,17 +1245,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(); /** * handles paste status/result * * @name PasteStatus * @param {object} window - * @param {object} document * @class */ - var PasteStatus = (function (window, document) { + var PasteStatus = (function (window) { var me = {}; var $pasteSuccess, @@ -1402,17 +1397,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(window); /** * password prompt * * @name Prompt - * @param {object} window - * @param {object} document * @class */ - var Prompt = (function (window, document) { + var Prompt = (function () { var me = {}; var $passwordDecrypt, @@ -1512,7 +1505,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(); /** * Manage paste/message input, and preview tab @@ -1520,11 +1513,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * Note that the actual preview is handled by PasteViewer. * * @name Editor - * @param {object} window - * @param {object} document * @class */ - var Editor = (function (window, document) { + var Editor = (function () { var me = {}; var $editorTabs, @@ -1728,17 +1719,15 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(); /** * (view) Parse and show paste. * * @name PasteViewer - * @param {object} window - * @param {object} document * @class */ - var PasteViewer = (function (window, document) { + var PasteViewer = (function () { var me = {}; var $placeholder, @@ -1904,7 +1893,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @return {string} */ - me.getText = function(newText) + me.getText = function() { return text; } @@ -1981,7 +1970,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(); /** * (view) Show attachment and preview if possible @@ -1998,8 +1987,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $attachmentPreview, $attachment; - var attachmentChanged = false, - attachmentHasPreview = false; + var attachmentHasPreview = false; /** * sets the attachment but does not yet show it @@ -2027,8 +2015,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ); attachmentHasPreview = true; } - - attachmentChanged = true; } /** @@ -3043,7 +3029,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @private * @function * @param {int} status - * @param {int} data - optional + * @param {int} result - optional */ function success(status, result) { @@ -3063,7 +3049,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @private * @function * @param {int} status - internal code - * @param {int} data - original error code + * @param {int} result - original error code */ function fail(status, result) { @@ -3107,7 +3093,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name Uploader.setUrl * @function - * @param {function} func + * @param {function} newUrl */ me.setUrl = function(newUrl) { @@ -3236,17 +3222,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @return {array} */ me.parseUploadError = function(status, data, doThisThing) { - var errorArray = ['Error while parsing error message.']; + var errorArray; switch (status) { - case Uploader.error['custom']: + case me.error['custom']: errorArray = ['Could not ' + doThisThing + ': %s', data.message]; break; - case Uploader.error['unknown']: + case me.error['unknown']: errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')]; break; - case Uploader.error['serverError']: - errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')]; break; + case me.error['serverError']: + errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')]; + break; default: errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')]; break; @@ -3884,7 +3871,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show proper elements on screen PasteDecrypter.run(); - return; } /** diff --git a/lib/Data/AbstractData.php b/lib/Data/AbstractData.php index c5eae21..41260f8 100644 --- a/lib/Data/AbstractData.php +++ b/lib/Data/AbstractData.php @@ -58,7 +58,7 @@ abstract class AbstractData * @access public * @static * @param array $options - * @return privatebin_abstract + * @return AbstractData */ public static function getInstance($options) { @@ -88,7 +88,6 @@ abstract class AbstractData * * @access public * @param string $pasteid - * @return void */ abstract public function delete($pasteid); @@ -147,7 +146,6 @@ abstract class AbstractData * * @access public * @param int $batchsize - * @return void */ public function purge($batchsize) { diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 6674484..c35df3b 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -282,7 +282,6 @@ class Database extends AbstractData * * @access public * @param string $pasteid - * @return void */ public function delete($pasteid) { @@ -375,11 +374,10 @@ class Database extends AbstractData $comments[$i]->data = $row['data']; $comments[$i]->meta = new stdClass; $comments[$i]->meta->postdate = (int) $row['postdate']; - if (array_key_exists('nickname', $row) && !empty($row['nickname'])) { - $comments[$i]->meta->nickname = $row['nickname']; - } - if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) { - $comments[$i]->meta->vizhash = $row['vizhash']; + foreach (array('nickname', 'vizhash') as $key) { + if (array_key_exists($key, $row) && !empty($row[$key])) { + $comments[$i]->meta->$key = $row[$key]; + } } } ksort($comments); @@ -564,7 +562,6 @@ class Database extends AbstractData * * @access private * @static - * @return void */ private static function _createPasteTable() { @@ -589,7 +586,6 @@ class Database extends AbstractData * * @access private * @static - * @return void */ private static function _createCommentTable() { @@ -616,7 +612,6 @@ class Database extends AbstractData * * @access private * @static - * @return void */ private static function _createConfigTable() { @@ -651,7 +646,6 @@ class Database extends AbstractData * @access private * @static * @param string $oldversion - * @return void */ private static function _upgradeDatabase($oldversion) { diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index 0984405..4100e29 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -98,7 +98,6 @@ class Filesystem extends AbstractData * * @access public * @param string $pasteid - * @return void */ public function delete($pasteid) { diff --git a/lib/I18n.php b/lib/I18n.php index d35bcf0..2bee73e 100644 --- a/lib/I18n.php +++ b/lib/I18n.php @@ -135,15 +135,17 @@ class I18n * * @access public * @static - * @return void */ public static function loadTranslations() { $availableLanguages = self::getAvailableLanguages(); // check if the lang cookie was set and that language exists - if (array_key_exists('lang', $_COOKIE) && in_array($_COOKIE['lang'], $availableLanguages)) { - $match = $_COOKIE['lang']; + if ( + array_key_exists('lang', $_COOKIE) && + ($key = array_search($_COOKIE['lang'], $availableLanguages)) !== false + ) { + $match = $availableLanguages[$key]; } // find a translation file matching the browsers language preferences else { @@ -256,7 +258,6 @@ class I18n * @access public * @static * @param string $lang - * @return void */ public static function setLanguageFallback($lang) { diff --git a/lib/Model.php b/lib/Model.php index fc79569..d1011f1 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -40,7 +40,6 @@ class Model * Factory constructor. * * @param configuration $conf - * @return void */ public function __construct(Configuration $conf) { @@ -64,8 +63,6 @@ class Model /** * Checks if a purge is necessary and triggers it if yes. - * - * @return void */ public function purge() { diff --git a/lib/Model/AbstractModel.php b/lib/Model/AbstractModel.php index 3dd48a8..55956b7 100644 --- a/lib/Model/AbstractModel.php +++ b/lib/Model/AbstractModel.php @@ -63,7 +63,6 @@ abstract class AbstractModel * @access public * @param Configuration $configuration * @param AbstractData $storage - * @return void */ public function __construct(Configuration $configuration, AbstractData $storage) { @@ -90,7 +89,6 @@ abstract class AbstractModel * @access public * @param string $id * @throws Exception - * @return void */ public function setId($id) { @@ -106,7 +104,6 @@ abstract class AbstractModel * @access public * @param string $data * @throws Exception - * @return void */ public function setData($data) { @@ -133,7 +130,6 @@ abstract class AbstractModel * * @access public * @throws Exception - * @return void */ abstract public function store(); @@ -142,7 +138,6 @@ abstract class AbstractModel * * @access public * @throws Exception - * @return void */ abstract public function delete(); diff --git a/lib/Model/Comment.php b/lib/Model/Comment.php index 86f4ffa..b67742d 100644 --- a/lib/Model/Comment.php +++ b/lib/Model/Comment.php @@ -61,7 +61,6 @@ class Comment extends AbstractModel * * @access public * @throws Exception - * @return void */ public function store() { @@ -101,7 +100,6 @@ class Comment extends AbstractModel * * @access public * @throws Exception - * @return void */ public function delete() { @@ -129,7 +127,6 @@ class Comment extends AbstractModel * @access public * @param Paste $paste * @throws Exception - * @return void */ public function setPaste(Paste $paste) { @@ -154,7 +151,6 @@ class Comment extends AbstractModel * @access public * @param string $id * @throws Exception - * @return void */ public function setParentId($id) { @@ -184,7 +180,6 @@ class Comment extends AbstractModel * @access public * @param string $nickname * @throws Exception - * @return void */ public function setNickname($nickname) { diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index 038bfbc..8f171fe 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -75,7 +75,6 @@ class Paste extends AbstractModel * * @access public * @throws Exception - * @return void */ public function store() { @@ -103,7 +102,6 @@ class Paste extends AbstractModel * * @access public * @throws Exception - * @return void */ public function delete() { @@ -183,7 +181,6 @@ class Paste extends AbstractModel * @access public * @param string $attachment * @throws Exception - * @return void */ public function setAttachment($attachment) { @@ -199,7 +196,6 @@ class Paste extends AbstractModel * @access public * @param string $attachmentname * @throws Exception - * @return void */ public function setAttachmentName($attachmentname) { @@ -214,7 +210,6 @@ class Paste extends AbstractModel * * @access public * @param string $expiration - * @return void */ public function setExpiration($expiration) { @@ -236,7 +231,6 @@ class Paste extends AbstractModel * @access public * @param string $burnafterreading * @throws Exception - * @return void */ public function setBurnafterreading($burnafterreading = '1') { @@ -257,7 +251,6 @@ class Paste extends AbstractModel * @access public * @param string $opendiscussion * @throws Exception - * @return void */ public function setOpendiscussion($opendiscussion = '1') { @@ -281,7 +274,6 @@ class Paste extends AbstractModel * @access public * @param string $format * @throws Exception - * @return void */ public function setFormatter($format) { diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 68f148f..64fb530 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -36,7 +36,6 @@ abstract class AbstractPersistence * @access public * @static * @param string $path - * @return void */ public static function setPath($path) { @@ -80,7 +79,6 @@ abstract class AbstractPersistence * @access protected * @static * @throws Exception - * @return void */ protected static function _initialize() { diff --git a/lib/Persistence/PurgeLimiter.php b/lib/Persistence/PurgeLimiter.php index a383007..2eb0b52 100644 --- a/lib/Persistence/PurgeLimiter.php +++ b/lib/Persistence/PurgeLimiter.php @@ -36,7 +36,6 @@ class PurgeLimiter extends AbstractPersistence * @access public * @static * @param int $limit - * @return void */ public static function setLimit($limit) { @@ -49,7 +48,6 @@ class PurgeLimiter extends AbstractPersistence * @access public * @static * @param Configuration $conf - * @return void */ public static function setConfiguration(Configuration $conf) { diff --git a/lib/Persistence/ServerSalt.php b/lib/Persistence/ServerSalt.php index 451fbd6..129a099 100644 --- a/lib/Persistence/ServerSalt.php +++ b/lib/Persistence/ServerSalt.php @@ -95,7 +95,6 @@ class ServerSalt extends AbstractPersistence * @access public * @static * @param string $path - * @return void */ public static function setPath($path) { diff --git a/lib/Persistence/TrafficLimiter.php b/lib/Persistence/TrafficLimiter.php index a908a82..914450a 100644 --- a/lib/Persistence/TrafficLimiter.php +++ b/lib/Persistence/TrafficLimiter.php @@ -45,7 +45,6 @@ class TrafficLimiter extends AbstractPersistence * @access public * @static * @param int $limit - * @return void */ public static function setLimit($limit) { @@ -58,7 +57,6 @@ class TrafficLimiter extends AbstractPersistence * @access public * @static * @param Configuration $conf - * @return void */ public static function setConfiguration(Configuration $conf) { diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index 92072ea..c817445 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -123,7 +123,6 @@ class PrivateBin * * @access public * @throws Exception - * @return void */ public function __construct() { @@ -171,7 +170,6 @@ class PrivateBin * initialize privatebin * * @access private - * @return void */ private function _init() { @@ -320,7 +318,6 @@ class PrivateBin * @access private * @param string $dataid * @param string $deletetoken - * @return void */ private function _delete($dataid, $deletetoken) { @@ -364,7 +361,6 @@ class PrivateBin * * @access private * @param string $dataid - * @return void */ private function _read($dataid) { @@ -397,7 +393,6 @@ class PrivateBin * Display PrivateBin frontend. * * @access private - * @return void */ private function _view() { @@ -461,7 +456,6 @@ class PrivateBin * * @access private * @param string $type - * @return void */ private function _jsonld($type) { @@ -494,7 +488,6 @@ class PrivateBin * @param int $status * @param string $message * @param array $other - * @return void */ private function _return_message($status, $message, $other = array()) { diff --git a/lib/Request.php b/lib/Request.php index e6c1c74..37c0bca 100644 --- a/lib/Request.php +++ b/lib/Request.php @@ -41,7 +41,7 @@ class Request const MIME_XHTML = 'application/xhtml+xml'; /** - * Input stream to use for PUT parameter parsing. + * Input stream to use for PUT parameter parsing * * @access private * @var string @@ -49,7 +49,7 @@ class Request private static $_inputStream = 'php://input'; /** - * Operation to perform. + * Operation to perform * * @access private * @var string @@ -57,7 +57,7 @@ class Request private $_operation = 'view'; /** - * Request parameters. + * Request parameters * * @access private * @var array @@ -65,7 +65,7 @@ class Request private $_params = array(); /** - * If we are in a JSON API context. + * If we are in a JSON API context * * @access private * @var bool @@ -73,10 +73,9 @@ class Request private $_isJsonApi = false; /** - * Constructor. + * Constructor * * @access public - * @return void */ public function __construct() { @@ -122,7 +121,7 @@ class Request } /** - * Get current operation. + * Get current operation * * @access public * @return string @@ -133,7 +132,7 @@ class Request } /** - * Get a request parameter. + * Get a request parameter * * @access public * @param string $param @@ -146,7 +145,7 @@ class Request } /** - * If we are in a JSON API context. + * If we are in a JSON API context * * @access public * @return bool @@ -157,7 +156,7 @@ class Request } /** - * Override the default input stream source, used for unit testing. + * Override the default input stream source, used for unit testing * * @param string $input */ @@ -167,7 +166,7 @@ class Request } /** - * detect the clients supported media type and decide if its a JSON API call or not + * Detect the clients supported media type and decide if its a JSON API call or not * * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 * diff --git a/lib/View.php b/lib/View.php index d7ecaa2..6c04e47 100644 --- a/lib/View.php +++ b/lib/View.php @@ -35,7 +35,6 @@ class View * @access public * @param string $name * @param mixed $value - * @return void */ public function assign($name, $value) { @@ -48,7 +47,6 @@ class View * @access public * @param string $template * @throws Exception - * @return void */ public function draw($template) { diff --git a/lib/Vizhash16x16.php b/lib/Vizhash16x16.php index 604c86e..e9bd5d0 100644 --- a/lib/Vizhash16x16.php +++ b/lib/Vizhash16x16.php @@ -61,7 +61,6 @@ class Vizhash16x16 * constructor * * @access public - * @return void */ public function __construct() { @@ -210,7 +209,6 @@ class Vizhash16x16 * @param resource $image * @param int $action * @param int $color - * @return void */ private function drawshape($image, $action, $color) { diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index a80e175..7ee8d0d 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + diff --git a/tpl/page.php b/tpl/page.php index 0090630..2fd511f 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - +