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 | 1110 +++++++++++++++++++++++++-------------------- tpl/bootstrap.php | 2 +- 3 files changed, 613 insertions(+), 501 deletions(-) diff --git a/.eslintrc b/.eslintrc index 97437c73..a5e0c90e 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 4fd0e99b..ed3baa3b 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'); + $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'); - this.loadingIndicator.removeClass('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'); + $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'); - this.newButton.removeClass('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'); + $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'); } - this.pasteResult.addClass('hidden'); - this.message.addClass('hidden'); - this.clearText.addClass('hidden'); - this.prettyMessage.addClass('hidden'); - this.loadingIndicator.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 60c6727d..698d3594 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 ed3baa3b..85c71b69 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 | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 85c71b69..6322c0e8 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'); + commentText = filter.decipher(key, password, comment.data), + $parentComment = $('#comment_' + comment.parentid); - // if the element exists in page - if ($(cname).length) + $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 | 3874 +++++++++++++++++++++++----------------------- 1 file changed, 1935 insertions(+), 1939 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 6322c0e8..adb26fd9 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 -// } - -jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { - /** - * static helper methods - * - * @param {object} window - * @param {object} document - * @name helper - * @class - */ - 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 - * - * @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']; - }; - - /** - * 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); - } - }; - - /** - * 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 - { - $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) - { - 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: - *
-         *     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) - { - 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) === ' ') - { - c = c.substring(1); - } - if (c.indexOf(name) === 0) - { - 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 - ); - - var hashIndex = scriptLocation.indexOf('?'); - - if (hashIndex !== -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() - { - var key = window.location.hash.substring(1), - i = key.indexOf('&'); - - // Some web 2.0 services and redirectors add data AFTER the anchor - // (such as &utm_source=...). We will strip any additional data. - if (i > -1) - { - key = key.substring(0, i); - } - - return key; - }; - - /** - * 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); - - /** - * internationalization methods - * - * @param {object} window - * @param {object} document - * @name i18n - * @class - */ - 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); - }; - - /** - * 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') - { - 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])) - { - 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); - }; - - /** - * 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 = helper.getCookie('lang'); - - // 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 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); - }); - - controller.init(); - }; - - return me; - })(window, document); - - /** - * filter methods - * - * @param {object} window - * @param {object} document - * @name filter - * @class - */ - 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) - { - 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 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) - { - try - { - return me.decompress(sjcl.decrypt(key, data)); - } - catch(err) - { - 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 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 = ''; - - // 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 { - $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'); - - $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') - { - prettyPrint(); - } - $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'); - - $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) - { - 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) - { - $image.html( - $(document.createElement('img')) - .attr('src', attachment) - .attr('class', 'img-thumbnail') - ); - $image.removeClass('hidden'); - } - } - var cleartext = filter.decipher(key, password, paste.data); - if (cleartext.length === 0 && password.length === 0 && !paste.attachment) - { - me.requestPassword(); - return; - } - if (cleartext.length === 0 && !paste.attachment) - { - throw 'failed to decipher message'; - } - - $passwordInput.val(password); - if (cleartext.length > 0) - { - $('#pasteFormatter').val(paste.meta.formatter); - me.formatPaste(paste.meta.formatter, cleartext); - } - } - catch(err) - { - me.stateOnlyNewPaste(); - me.showError(i18n._('Could not decrypt data (Wrong key?)')); - return; - } - } - - // display paste expiration / for your eyes only - if (paste.meta.expire_date) - { - var expiration = helper.secondsToHuman(paste.meta.remaining_time), - expirationLabel = [ - 'This document will expire in %d ' + expiration[1] + '.', - 'This document will expire in %d ' + expiration[1] + 's.' - ]; - 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) - { - 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'); - } - }; - - /** - * 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) - { - 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 - }; - - $.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, - 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'))); - } - } - }) - .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 ''; - }; - - /** - * 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(); - }; - - /** - * 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'); - }; - - /** - * 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'); - }; - - /** - * 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) - { - // no "clone" for IE<10. - if ($('#oldienotice').is(":visible")) - { - $cloneButton.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'); - }; - - /** - * when "burn after reading" is checked, disable discussion - * - * @name controller.changeBurnAfterReading - * @function - */ - me.changeBurnAfterReading = function() - { - if ($burnAfterReading.is(':checked') ) - { - $openDisc.addClass('buttondisabled'); - $openDiscussion.attr({checked: false, disabled: true}); - } - else - { - $openDisc.removeClass('buttondisabled'); - $openDiscussion.removeAttr('disabled'); - } - }; - - /** - * when discussion is checked, disable "burn after reading" - * - * @name controller.changeOpenDisc - * @function - */ - me.changeOpenDisc = function() - { - if ($openDiscussion.is(':checked') ) - { - $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(); - }; - - /** - * 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')) - { - $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(); - }; - - /** - * 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; - } - }; - - /** - * 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(); - }; - - /** - * 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) - { - $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) - { - var img = ''; - $status.prepend(img); - if (typeof $replyStatus !== 'undefined') { - $replyStatus.prepend(img); - } - } - 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); - }; - - /** - * 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) - { - me.showStatus($status.text()); - return; - } - - // keep line height even if content empty - $status.html(' '); - - // 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); - +// startup +jQuery(document).ready(function() { /** * main application start, called when DOM is fully loaded and * runs controller initalization after translations are loaded */ - $(i18n.loadTranslations); + PrivateBin.i18n.loadTranslations(); +}); - return { - helper: helper, - i18n: i18n, - filter: filter, - controller: controller +/** + * @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, sjcl, Base64, RawDeflate); + + /** + * 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']; + }; + + /** + * 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); + } + }; + + /** + * 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 + { + $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) + { + 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: + *
+     *     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) + { + 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) === ' ') + { + c = c.substring(1); + } + if (c.indexOf(name) === 0) + { + 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 + ); + + var hashIndex = scriptLocation.indexOf('?'); + + if (hashIndex !== -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() + { + var key = window.location.hash.substring(1), + i = key.indexOf('&'); + + // Some web 2.0 services and redirectors add data AFTER the anchor + // (such as &utm_source=...). We will strip any additional data. + if (i > -1) + { + key = key.substring(0, i); + } + + return key; + }; + + /** + * 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 = {}; + + /** + * 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); + }; + + /** + * 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') + { + 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])) + { + 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); + }; + + /** + * 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); + } + + // 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(); + } + + // 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(); + }; + + 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 = {}; + + /** + * 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) + { + 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 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) + { + try + { + return me.decompress(sjcl.decrypt(key, data)); + } + catch(err) + { + 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 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; + + /** + * 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(); + } + }; + + /** + * 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') + { + prettyPrint(); + } + $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'); + + $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) + { + 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) + { + $image.html( + $(document.createElement('img')) + .attr('src', attachment) + .attr('class', 'img-thumbnail') + ); + $image.removeClass('hidden'); + } + } + var cleartext = filter.decipher(key, password, paste.data); + if (cleartext.length === 0 && password.length === 0 && !paste.attachment) + { + me.requestPassword(); + return; + } + if (cleartext.length === 0 && !paste.attachment) + { + throw 'failed to decipher message'; + } + + $passwordInput.val(password); + if (cleartext.length > 0) + { + $('#pasteFormatter').val(paste.meta.formatter); + me.formatPaste(paste.meta.formatter, cleartext); + } + } + catch(err) + { + me.stateOnlyNewPaste(); + me.showError(i18n._('Could not decrypt data (Wrong key?)')); + return; + } + } + + // display paste expiration / for your eyes only + if (paste.meta.expire_date) + { + var expiration = helper.secondsToHuman(paste.meta.remaining_time), + expirationLabel = [ + 'This document will expire in %d ' + expiration[1] + '.', + 'This document will expire in %d ' + expiration[1] + 's.' + ]; + 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) + { + 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'); + } + }; + + /** + * 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) + { + 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 + }; + + $.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, + 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'))); + } + } + }) + .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 ''; + }; + + /** + * 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(); + }; + + /** + * 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'); + }; + + /** + * 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'); + }; + + /** + * 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) + { + // no "clone" for IE<10. + if ($('#oldienotice').is(":visible")) + { + $cloneButton.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'); + }; + + /** + * when "burn after reading" is checked, disable discussion + * + * @name controller.changeBurnAfterReading + * @function + */ + me.changeBurnAfterReading = function() + { + if ($burnAfterReading.is(':checked') ) + { + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); + } + else + { + $openDisc.removeClass('buttondisabled'); + $openDiscussion.removeAttr('disabled'); + } + }; + + /** + * when discussion is checked, disable "burn after reading" + * + * @name controller.changeOpenDisc + * @function + */ + me.changeOpenDisc = function() + { + if ($openDiscussion.is(':checked') ) + { + $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(); + }; + + /** + * 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')) + { + $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(); + }; + + /** + * 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; + } + }; + + /** + * 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(); + }; + + /** + * 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) + { + $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) + { + var img = ''; + $status.prepend(img); + if (typeof $replyStatus !== 'undefined') { + $replyStatus.prepend(img); + } + } + 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); + } + + /** + * 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) + { + me.showStatus($status.text()); + return; + } + + // keep line height even if content empty + $status.html(' '); + + // 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, 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 | 3798 +++++++++++++++++++++++----------------------- 1 file changed, 1901 insertions(+), 1897 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index adb26fd9..6322c0e8 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() { +// jQuery(document).ready(function() { +// // startup +// } + +jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** - * 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 + * static helper methods * - * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} - * @private - * @enum {Object} - * @readonly + * @param {object} window + * @param {object} document + * @name helper + * @class */ - 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']; - }; - - /** - * 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); - } - }; - - /** - * 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 - { - $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) - { - 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: - *
-     *     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) - { - 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) === ' ') - { - c = c.substring(1); - } - if (c.indexOf(name) === 0) - { - 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 - ); - - var hashIndex = scriptLocation.indexOf('?'); - - if (hashIndex !== -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() - { - var key = window.location.hash.substring(1), - i = key.indexOf('&'); - - // Some web 2.0 services and redirectors add data AFTER the anchor - // (such as &utm_source=...). We will strip any additional data. - if (i > -1) - { - key = key.substring(0, i); - } - - return key; - }; - - /** - * 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 = {}; - - /** - * 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); - }; - - /** - * 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') - { - 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])) - { - 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); - }; - - /** - * 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); - } - - // 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(); - } - - // 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(); - }; - - 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 = {}; - - /** - * 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) - { - 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 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) - { - try - { - return me.decompress(sjcl.decrypt(key, data)); - } - catch(err) - { - 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 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; - - /** - * 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(); - } - }; - - /** - * 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') - { - prettyPrint(); - } - $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'); - - $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) - { - 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) - { - $image.html( - $(document.createElement('img')) - .attr('src', attachment) - .attr('class', 'img-thumbnail') - ); - $image.removeClass('hidden'); - } - } - var cleartext = filter.decipher(key, password, paste.data); - if (cleartext.length === 0 && password.length === 0 && !paste.attachment) - { - me.requestPassword(); - return; - } - if (cleartext.length === 0 && !paste.attachment) - { - throw 'failed to decipher message'; - } - - $passwordInput.val(password); - if (cleartext.length > 0) - { - $('#pasteFormatter').val(paste.meta.formatter); - me.formatPaste(paste.meta.formatter, cleartext); - } - } - catch(err) - { - me.stateOnlyNewPaste(); - me.showError(i18n._('Could not decrypt data (Wrong key?)')); - return; - } - } - - // display paste expiration / for your eyes only - if (paste.meta.expire_date) - { - var expiration = helper.secondsToHuman(paste.meta.remaining_time), - expirationLabel = [ - 'This document will expire in %d ' + expiration[1] + '.', - 'This document will expire in %d ' + expiration[1] + 's.' - ]; - 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) - { - 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'); - } - }; - - /** - * 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) - { - 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 + 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 = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' }; - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) + /** + * 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) { - 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'))); - } + v = Math.floor(seconds); + return [v, 'second']; } - }) - .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) + if (seconds < 60 * 60) { - // revert loading status… - me.stateNewPaste(); - me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); - return; + v = Math.floor(seconds / 60); + return [v, 'minute']; } - var reader = new FileReader(); - // closure to capture the file information - reader.onload = (function(theFile) + if (seconds < 60 * 60 * 24) { - 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; + v = Math.floor(seconds / (60 * 60)); + return [v, 'hour']; } - } - $.ajax({ - type: 'POST', - url: helper.scriptLocation(), - data: data_to_send, - dataType: 'json', - headers: headers, - success: function(data) + // If less than 2 months, display in days: + if (seconds < 60 * 60 * 24 * 60) { - 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'))); - } + v = Math.floor(seconds / (60 * 60 * 24)); + return [v, 'day']; } - }) - .fail(function() + 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) { - // revert loading status… - me.stateNewPaste(); - controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); - }); - }; + var range, selection; - /** - * 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 ''; - }; - - /** - * 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(); - }; - - /** - * 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'); - }; - - /** - * 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'); - }; - - /** - * 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) - { - // no "clone" for IE<10. - if ($('#oldienotice').is(":visible")) + // MS + if (document.body.createTextRange) { - $cloneButton.addClass('hidden'); + 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); + } + }; + + /** + * 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 + { + $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) + { + var content = $element.contents(); + if (content.length > 0) + { + content[content.length - 1].nodeValue = ' ' + message; } else { - $cloneButton.removeClass('hidden'); + 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 + ) + ); + }; + + /** + * 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) + { + 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) === ' ') + { + c = c.substring(1); + } + if (c.indexOf(name) === 0) + { + 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; } - $rawTextButton.removeClass('hidden'); + scriptLocation = window.location.href.substring( + 0, + window.location.href.length - window.location.search.length - window.location.hash.length + ); + + var hashIndex = scriptLocation.indexOf('?'); + + if (hashIndex !== -1) + { + scriptLocation = scriptLocation.substring(0, hashIndex); + } + + return scriptLocation; + }; + + /** + * get the pastes unique identifier from the URL, + * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487 + * + * @name helper.pasteId + * @function + * @return {string} unique identifier + */ + me.pasteId = function() + { + return window.location.search.substring(1); + }; + + /** + * return the deciphering key stored in anchor part of the URL + * + * @name helper.pageKey + * @function + * @return {string} key + */ + me.pageKey = function() + { + var key = window.location.hash.substring(1), + i = key.indexOf('&'); + + // Some web 2.0 services and redirectors add data AFTER the anchor + // (such as &utm_source=...). We will strip any additional data. + if (i > -1) + { + key = key.substring(0, i); + } + + return key; + }; + + /** + * 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); + + /** + * internationalization methods + * + * @param {object} window + * @param {object} document + * @name i18n + * @class + */ + 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); + }; + + /** + * 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') + { + 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])) + { + 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); + }; + + /** + * 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 = helper.getCookie('lang'); + + // 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 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); + }); + + controller.init(); + }; + + return me; + })(window, document); + + /** + * filter methods + * + * @param {object} window + * @param {object} document + * @name filter + * @class + */ + 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) + { + 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 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) + { + try + { + return me.decompress(sjcl.decrypt(key, data)); + } + catch(err) + { + 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 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 = ''; + + // 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 { + $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'); + + $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') + { + prettyPrint(); + } + $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'); + + $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) + { + 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) + { + $image.html( + $(document.createElement('img')) + .attr('src', attachment) + .attr('class', 'img-thumbnail') + ); + $image.removeClass('hidden'); + } + } + var cleartext = filter.decipher(key, password, paste.data); + if (cleartext.length === 0 && password.length === 0 && !paste.attachment) + { + me.requestPassword(); + return; + } + if (cleartext.length === 0 && !paste.attachment) + { + throw 'failed to decipher message'; + } + + $passwordInput.val(password); + if (cleartext.length > 0) + { + $('#pasteFormatter').val(paste.meta.formatter); + me.formatPaste(paste.meta.formatter, cleartext); + } + } + catch(err) + { + me.stateOnlyNewPaste(); + me.showError(i18n._('Could not decrypt data (Wrong key?)')); + return; + } + } + + // display paste expiration / for your eyes only + if (paste.meta.expire_date) + { + var expiration = helper.secondsToHuman(paste.meta.remaining_time), + expirationLabel = [ + 'This document will expire in %d ' + expiration[1] + '.', + 'This document will expire in %d ' + expiration[1] + 's.' + ]; + 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) + { + 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'); + } + }; + + /** + * 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) + { + 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 + }; + + $.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, + 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'))); + } + } + }) + .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 ''; + }; + + /** + * 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(); + }; + + /** + * 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'); - $attach.addClass('hidden'); $expiration.addClass('hidden'); $formatter.addClass('hidden'); $burnAfterReadingOption.addClass('hidden'); $openDisc.addClass('hidden'); - $newButton.removeClass('hidden'); + $newButton.addClass('hidden'); + $password.addClass('hidden'); + $attach.addClass('hidden'); + $message.addClass('hidden'); $preview.addClass('hidden'); - } - $pasteResult.addClass('hidden'); - $message.addClass('hidden'); - $clearText.addClass('hidden'); - $prettyMessage.addClass('hidden'); - $loadingIndicator.addClass('hidden'); - }; + $loadingIndicator.removeClass('hidden'); + }; - /** - * when "burn after reading" is checked, disable discussion - * - * @name controller.changeBurnAfterReading - * @function - */ - me.changeBurnAfterReading = function() - { - if ($burnAfterReading.is(':checked') ) + /** + * put the screen in a state where the only option is to submit a + * new paste + * + * @name controller.stateOnlyNewPaste + * @function + */ + me.stateOnlyNewPaste = function() { - $openDisc.addClass('buttondisabled'); - $openDiscussion.attr({checked: false, disabled: true}); - } - else + $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 + * + * @name controller.stateExistingPaste + * @function + * @param {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false + */ + me.stateExistingPaste = function(preview) { - $openDisc.removeClass('buttondisabled'); - $openDiscussion.removeAttr('disabled'); - } - }; + preview = preview || false; - /** - * when discussion is checked, disable "burn after reading" - * - * @name controller.changeOpenDisc - * @function - */ - me.changeOpenDisc = function() - { - if ($openDiscussion.is(':checked') ) - { - $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(); - }; - - /** - * 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')) - { - $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(); - }; - - /** - * 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; - } - }; - - /** - * 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(); - }; - - /** - * 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) + if (!preview) { - $replyStatus.html($status.html()); + // no "clone" for IE<10. + if ($('#oldienotice').is(":visible")) + { + $cloneButton.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'); + }; + + /** + * when "burn after reading" is checked, disable discussion + * + * @name controller.changeBurnAfterReading + * @function + */ + me.changeBurnAfterReading = function() + { + if ($burnAfterReading.is(':checked') ) + { + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); } 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 - * - * @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) + /** + * when discussion is checked, disable "burn after reading" + * + * @name controller.changeOpenDisc + * @function + */ + me.changeOpenDisc = function() { - var img = ''; - $status.prepend(img); - if (typeof $replyStatus !== 'undefined') { - $replyStatus.prepend(img); - } - } - 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); - } - - /** - * 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) - { - me.showStatus($status.text()); - return; - } - - // keep line height even if content empty - $status.html(' '); - - // display an existing paste - if ($cipherData.text().length > 1) - { - // missing decryption key in URL? - if (window.location.hash.length === 0) + if ($openDiscussion.is(':checked') ) { - 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?)')); + $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(); + }; + + /** + * 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')) + { + $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(); + }; + + /** + * 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; + } + }; + + /** + * 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(); + }; + + /** + * 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) + { + $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) + { + var img = ''; + $status.prepend(img); + if (typeof $replyStatus !== 'undefined') { + $replyStatus.prepend(img); + } + } + 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); + }; + + /** + * 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) + { + 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 60c6727d..4bf3ca1d 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 c92136f3..4ae0b6a7 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 | 1694 +++++++++++++++++++++++++-------------------- tpl/bootstrap.php | 23 +- 2 files changed, 974 insertions(+), 743 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 6322c0e8..0416a03f 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(); }; + /** + * replace last child of element with message + * + * @name me.appendMessage + * @function + * @param {jQuery} $element - a jQuery wrapped DOM element + * @param {string} message - the message to append + * @TODO: make private if possible + */ + me.appendMessage = function($element, message) + { + var content = $element.contents(); + if (content.length > 0) + { + content[content.length - 1].nodeValue = ' ' + message; + } + else + { + me.setElementText($element, 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); + } + }; + + /** + * 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(); + }; + + /** + * main UI manager + * + * @name controller.init + * @function + */ + me.init = function() + { + // hide "no javascript" message + $('#noscript').hide(); + + // 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'); + + // bind events + $('.reloadlink').click(me.reloadPage); + + // 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 + * + * @name controller.stateNewPaste + * @function + */ + me.stateNewPaste = function() + { + $remainingTime.removeClass('hidden'); + + $loadingIndicator.addClass('hidden'); + console.error('stateNewPaste is depreciated'); + }; + + /** + * put the screen in mode after submitting a paste + * + * @name controller.stateSubmittingPaste + * @function + */ + me.stateSubmittingPaste = function() + { + console.error('stateSubmittingPaste is depreciated'); + }; + + /** + * put the screen in a state where the only option is to submit a + * new paste + * + * @name controller.stateOnlyNewPaste + * @function + */ + me.stateOnlyNewPaste = function() + { + console.error('stateOnlyNewPaste is depreciated'); + }; + + /** + * 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; + console.error('stateExistingPaste is depreciated'); + + if (!preview) + { + // no "clone" for IE<10. + if ($('#oldienotice').is(":visible")) + { + $cloneButton.addClass('hidden'); + } + else + { + $cloneButton.removeClass('hidden'); + } + + console.log('show no preview'); + } + }; + + 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; + + /** + * display a status message + * + * @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) + { + // spin is ignored for now + $status.text(message); + }; + + // @TODO: add showLoading() + + /** + * display a status message for replying to comments + * + * @name controller.showStatus + * @function + * @param {string} message - text to display + * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false + */ + me.showReplyStatus = function(message, spin) + { + if (spin || false) { + $replyStatus.find('.spinner').removeClass('hidden') + } + $replyStatus.text(message); + }; + + /** + * hides any status messages + * + * @name controller.hideSTatus + * @function + */ + me.hideStatus = function() + { + $status.html(' '); + }; + + /** + * display an error message + * + * @name controller.showError + * @function + * @param {string} message - text to display + */ + me.showError = function(message) + { + $errorMessage.removeClass('hidden'); + me.appendMessage($errorMessage, message); + }; + + /** + * display an error message + * + * @name controller.showError + * @function + * @param {string} message - text to display + */ + me.showReplyError = function(message) + { + $replyStatus.addClass('alert-danger'); + $replyStatus.addClass($errorMessage.attr('class')); // @TODO ???? + + $replyStatus.text(message); + }; + + /** + * init status manager + * + * preloads jQuery elements + * + * @name controller.init + * @function + */ + me.init = function() + { + // hide "no javascript" message + $('#noscript').hide(); + + $loadingIndicator = $('#loadingindicator'); // TODO: integrate $loadingIndicator into this module or leave it in state and remove it here + $errorMessage = $('#errormessage'); + $status = $('#status'); + // @TODO $replyStatus … + + // display status returned by php code, if any (eg. paste was properly deleted) + // @TODO remove this by handling errors in a different way + if ($status.text().length > 0) + { + me.showStatus($status.text()); + return; + } + + // keep line height even if content empty + $status.html(' '); // @TODO what? remove? + }; + + 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; + + /** + * 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(); + } + }; + + /** + * 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'); + }; + + + /** + * init status manager + * + * preloads jQuery elements + * + * @name controller.init + * @function + */ + me.init = function() + { + $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 editor.supportTabs + * @function + * @param {Event} event + * @TODO doc what is @this here? + * @TODO replace this with $message ?? + */ + function supportTabs(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; + } + } + + /** + * view the editor tab + * + * @name editor.viewEditor + * @function + * @param {Event} event + */ + function viewEditor(event) + { + $messagePreview.parent().removeClass('active'); + $messageEdit.parent().addClass('active'); + $message.focus(); + me.stateNewPaste(); + + event.preventDefault(); + } + + /** + * view the preview tab + * + * @name editor.viewPreview + * @function + * @param {Event} event + */ + function viewPreview(event) + { + $messageEdit.parent().removeClass('active'); + $messagePreview.parent().addClass('active'); + $message.focus(); + me.stateExistingPaste(true); + me.formatPaste($('#pasteFormatter').val(), $message.val()); + + event.preventDefault(); + } + + /** + * reset the editor view + * + * @name editor.reset + * @function + */ + me.reset = function() + { + // clear content + $message.text(''); + }; + + /** + * shows the editor + * + * @name editor.show + * @function + */ + me.show = function() + { + $attachment.removeClass('hidden'); + $clearText.removeClass('hidden'); + $discussion.removeClass('hidden'); + $pasteResult.removeClass('hidden'); //?? + // $prettyMessage.removeClass('hidden'); + $remainingTime.removeClass('hidden'); + }; + + /** + * hides the editor + * + * @name editor.reset + * @function + */ + me.hide = function() + { + $attachment.addClass('hidden'); + $clearText.addClass('hidden'); + $discussion.addClass('hidden'); + $pasteResult.addClass('hidden'); + $prettyMessage.addClass('hidden'); + $remainingTime.addClass('hidden'); + }; + + /** + * focuses the message input + * + * @name editor.focus + * @function + */ + me.focus = function() + { + $message.focus(); + }; + + /** + * init status manager + * + * preloads jQuery elements + * + * @name editor.init + * @function + */ + me.init = function() + { + $message = $('#message'); + $messageEdit = $('#messageedit'); + $messagePreview = $('#messagepreview'); + + // bind events + $message.keydown(supportTabs); + $messageEdit.click(viewEditor); + $messagePreview.click(viewPreview); + }; + + 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(); + }; + + /** + * when "burn after reading" is checked, disable discussion + * + * @name topNav.changeBurnAfterReading + * @function + */ + function changeBurnAfterReading() + { + if ($burnAfterReading.is(':checked') ) + { + $openDisc.addClass('buttondisabled'); + $openDiscussion.attr({checked: false, disabled: true}); + } + else + { + $openDisc.removeClass('buttondisabled'); + $openDiscussion.removeAttr('disabled'); + } + } + + /** + * when discussion is checked, disable "burn after reading" + * + * @name topNav.changeOpenDisc + * @function + */ + function changeOpenDisc() + { + if ($openDiscussion.is(':checked') ) + { + $burnAfterReadingOption.addClass('buttondisabled'); + $burnAfterReading.attr({checked: false, disabled: true}); + } + else + { + $burnAfterReadingOption.removeClass('buttondisabled'); + $burnAfterReading.removeAttr('disabled'); + } + } + + /** + * 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'); + }; + + /** + * Shows all elements belonging to viwing an existing pastes + * + * @name topNav.hideAllElem + * @function + */ + me.showViewButtons = function() + { + $cloneButton.removeClass('hidden'); + $rawTextButton.removeClass('hidden'); + }; + + /** + * Hides all elements belonging to existing pastes + * + * @name topNav.hideAllElem + * @function + */ + me.hideViewButtons = function() + { + $cloneButton.addClass('hidden'); + $rawTextButton.addClass('hidden'); + }; + + /** + * 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'); + }; + + /** + * 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'); + }; + + /** + * only shows the "new paste" button + * + * @name topNav.setLanguage + * @function + */ + me.showNewPasteButton = function() + { + $newButton.addClass('hidden'); + }; + + /** + * shows a loading message, optionally with a percentage + * + * @name topNav.showLoading + * @function + * @param {string} message + * @param {int} percentage + */ + me.showLoading = function(message, percentage) + { + // currently parameters are ignored + $loadingIndicator.removeClass('hidden'); + }; + + /** + * hides the loading message + * + * @name topNav.hideLoading + * @function + */ + me.hideLoading = function() + { + $loadingIndicator.removeClass('hidden'); + }; + + /** + * init navigation manager + * + * preloads jQuery elements + * + * @name topNav.init + * @function + */ + me.init = function() + { + $attach = $('#attach'); + $attachment = $('#attachment'); + $attachmentLink = $('#attachment a'); + $burnAfterReading = $('#burnafterreading'); + $burnAfterReadingOption = $('#burnafterreadingoption'); + $cloneButton = $('#clonebutton'); + $expiration = $('#expiration'); + $fileRemoveButton = $('#fileremovebutton'); + $fileWrap = $('#filewrap'); + $formatter = $('#formatter'); + $newButton = $('#newbutton'); + $openDisc = $('#opendisc'); + $openDiscussion = $('#opendiscussion'); + $rawTextButton = $('#rawtextbutton'); + $sendButton = $('#sendbutton'); + + // 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 * @@ -1052,12 +1839,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showStatus(i18n._('Sending comment...'), true); var parentid = event.data.parentid, key = helper.pageKey(), - cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()), + cipherdata = cryptToolcipher(key, $passwordInput.val(), replyMessage.val()), ciphernickname = '', nick = $('#nickname').val(); if (nick.length > 0) { - ciphernickname = filter.cipher(key, $passwordInput.val(), nick); + ciphernickname = cryptToolcipher(key, $passwordInput.val(), nick); } var data_to_send = { data: cipherdata, @@ -1170,8 +1957,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return function(e) { controller.sendDataContinue( randomkey, - filter.cipher(randomkey, password, e.target.result), - filter.cipher(randomkey, password, theFile.name) + cryptToolcipher(randomkey, password, e.target.result), + cryptToolcipher(randomkey, password, theFile.name) ); }; })(files[0]); @@ -1181,7 +1968,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { me.sendDataContinue( randomkey, - filter.cipher(randomkey, password, $attachmentLink.attr('href')), + cryptToolcipher(randomkey, password, $attachmentLink.attr('href')), $attachmentLink.attr('download') ); } @@ -1202,7 +1989,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name) { - var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()), + var cipherdata = cryptToolcipher(randomkey, $passwordInput.val(), $message.val()), data_to_send = { data: cipherdata, expire: $('#pasteExpiration').val(), @@ -1230,7 +2017,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.stateExistingPaste(); var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey, deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - me.showStatus(''); + me.hideStatus(); $errorMessage.addClass('hidden'); // show new URL in browser bar history.pushState({type: 'newpaste'}, document.title, url); @@ -1254,7 +2041,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $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.hideStatus(); me.formatPaste(data_to_send.formatter, $message.val()); } else if (data.status === 1) @@ -1298,181 +2085,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return ''; }; - /** - * 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(); - }; - - /** - * 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'); - }; - - /** - * 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'); - }; - - /** - * 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) - { - // no "clone" for IE<10. - if ($('#oldienotice').is(":visible")) - { - $cloneButton.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'); - }; - - /** - * when "burn after reading" is checked, disable discussion - * - * @name controller.changeBurnAfterReading - * @function - */ - me.changeBurnAfterReading = function() - { - if ($burnAfterReading.is(':checked') ) - { - $openDisc.addClass('buttondisabled'); - $openDiscussion.attr({checked: false, disabled: true}); - } - else - { - $openDisc.removeClass('buttondisabled'); - $openDiscussion.removeAttr('disabled'); - } - }; - - /** - * when discussion is checked, disable "burn after reading" - * - * @name controller.changeOpenDisc - * @function - */ - me.changeOpenDisc = function() - { - if ($openDiscussion.is(':checked') ) - { - $burnAfterReadingOption.addClass('buttondisabled'); - $burnAfterReading.attr({checked: false, disabled: true}); - } - else - { - $burnAfterReadingOption.removeClass('buttondisabled'); - $burnAfterReading.removeAttr('disabled'); - } - }; - /** * forward to URL shortener * @@ -1486,45 +2098,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { 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(); - }; - - /** - * 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 * @@ -1534,13 +2107,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ 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(''); + status.hideStatus(); if ($attachmentLink.attr('href')) { $clonedFile.removeClass('hidden'); @@ -1551,157 +2123,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $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(); - }; - - /** - * 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; - } - }; - - /** - * 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(); - }; - - /** - * 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 * @@ -1711,222 +2136,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.newPaste = function() { me.stateNewPaste(); - me.showStatus(''); + me.hideStatus(); $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) - { - $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) - { - var img = ''; - $status.prepend(img); - if (typeof $replyStatus !== 'undefined') { - $replyStatus.prepend(img); - } - } - 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); - }; - - /** - * main application + * application start * * @name controller.init * @function */ me.init = function() { - // hide "no javascript" message - $('#noscript').hide(); + // first load translations + i18n.loadTranslations(); - // 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(' '); + // 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 698d3594..a2f5272b 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -404,19 +404,14 @@ if ($FILEUPLOAD):
-
-
+ if ($isCpct): + ?>
+
+
- + - + - + - - - - - -
+
+if ($isCpct): +?>
- +
@@ -465,12 +464,18 @@ endif;
diff --git a/tpl/page.php b/tpl/page.php index 4ae0b6a7..d7043bcf 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 2dc2e3c7..b10a3a39 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 dfb2d83e..1c7ec3cc 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 2fc88278..6508bec4 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 4c59ef57..d35bcf01 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 4bf3ca1d..04f2726b 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 4ae0b6a7..2bdbf913 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 04f2726b..cdd45258 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 a7abee7a..10b6a3a7 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 87def4ca..a80e175c 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 90c63850..00906305 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 97c4e245..61e0a3c6 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 0086f461..a259cb5d 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 df5aee2a..06bfb237 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 7e92da78..82ae2d10 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 06bfb237..30a80e97 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 82ae2d10..707a41b1 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 707a41b1..ae60abbd 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 41efcc45..d779c04e 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 ae60abbd..ca4c707b 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 5dbc5095..b627bc9e 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 632bf2b7..4248b5f4 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 9959e717..7974d773 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 1e2fd48f..e957c8f8 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 61e0a3c6..35cfd659 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 30a80e97..dc56fa75 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 4d92cc83..fd56583d 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 a29bce03..b6b449de 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 b9cc8f2a..99554981 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 e00a4a15..ab459524 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 2df26087..00b09702 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 d779c04e..5fcaf3db 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 fb3e523f..e874015f 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 61e0a3c6..c889eaf3 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 c889eaf3..35cfd659 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 9f09f532..a752f8cc 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 ca9befb3..393d3d80 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 9aaa70bc..68f148fd 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 e874015f..92072ea4 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 39ef6b92..00000000 --- a/tst/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ConfigurationCombinationsTest.php diff --git a/tst/.htaccess b/tst/.htaccess deleted file mode 100644 index b584d98c..00000000 --- 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 95029217..7cf5ee81 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 8f7a40b0..ac4e92fc 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 fbf5b603..a8aad11a 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 54cc30fc..a9d947ec 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 7cf5ee81..fe012c41 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 ac4e92fc..4d314f78 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 393d3d80..09844052 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 00000000..27c7131d --- /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 fe012c41..e7e6dc82 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 5cf13608..a5928893 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 f20209f5..29b0dade 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 27c7131d..26c8cebe 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 26c8cebe..56dde1a7 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 ab459524..06f72c51 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 10b6a3a7..9fd4626f 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 c5eae217..41260f89 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 66744848..c35df3b0 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 09844052..4100e291 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 d35bcf01..2bee73ec 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 fc795691..d1011f12 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 3dd48a83..55956b7a 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 86f4ffa1..b67742d2 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 038bfbc8..8f171fe1 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 68f148fd..64fb530c 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 a3830078..2eb0b52c 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 451fbd68..129a0992 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 a908a825..914450a9 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 92072ea4..c817445c 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 e6c1c749..37c0bca7 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 d7ecaa21..6c04e47f 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 604c86ef..e9bd5d0b 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 a80e175c..7ee8d0de 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 00906305..2fd511fa 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - +