From 2ebcf60516d2991d0869d10dcaad5b2924d804e2 Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 8 Feb 2017 13:20:51 +0100 Subject: [PATCH 01/79] 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/79] 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/79] 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/79] 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/79] 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 3ab489e92d9e073036f36182a30545628cd2e227 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 11 Feb 2017 09:09:47 +0100 Subject: [PATCH 06/79] added test for selectText function, but discovered that this can't be tested at the moment without a browser, due to jsdom lacking getSelect support --- js/test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/js/test.js b/js/test.js index b2d5f005..d9eb8703 100644 --- a/js/test.js +++ b/js/test.js @@ -66,6 +66,33 @@ describe('helper', function () { }); }); + // this test is not yet meaningful using jsdom, as it does not contain getSelection support. + // TODO: This needs to be tested using a browser. + describe('selectText', function () { + jsc.property( + 'selection contains content of given ID', + 'nearray string', + 'nearray nestring', + function (ids, contents) { + //console.log(ids, contents); + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + (contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + ids.forEach(function(item, i) { + $.PrivateBin.helper.selectText(item); + // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet. + // Once there is one, uncomment the line below to actually check the result. + //result *= (contents[i] || contents[0]) === window.getSelection().toString(); + }); + clean(); + return result; + } + ); + }); + describe('scriptLocation', function () { jsc.property( 'returns the URL without query & fragment', From 61a59911b8bf3f0e5aa647d6b870e8c9ebfbc2db Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 11 Feb 2017 09:56:56 +0100 Subject: [PATCH 07/79] added test for setElementText function --- js/test.js | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/js/test.js b/js/test.js index d9eb8703..93e7a8c1 100644 --- a/js/test.js +++ b/js/test.js @@ -71,24 +71,48 @@ describe('helper', function () { describe('selectText', function () { jsc.property( 'selection contains content of given ID', + jsc.nearray(jsc.nearray(jsc.elements(alnumString))), 'nearray string', - 'nearray nestring', function (ids, contents) { - //console.log(ids, contents); var html = '', result = true; ids.forEach(function(item, i) { - html += '
' + (contents[i] || contents[0]) + '
'; + html += '
' + (contents[i] || contents[0]) + '
'; }); var clean = jsdom(html); ids.forEach(function(item, i) { - $.PrivateBin.helper.selectText(item); + $.PrivateBin.helper.selectText(item.join('')); // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet. // Once there is one, uncomment the line below to actually check the result. //result *= (contents[i] || contents[0]) === window.getSelection().toString(); }); clean(); - return result; + return Boolean(result); + } + ); + }); + + describe('setElementText', function () { + jsc.property( + 'replaces the content of an element', + jsc.nearray(jsc.nearray(jsc.elements(alnumString))), + 'nearray string', + 'string', + function (ids, contents, replacingContent) { + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + (contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + ids.forEach(function(item, i) { + var id = item.join(''), + element = $(document.getElementById(id)); + $.PrivateBin.helper.setElementText(element, replacingContent); + result *= replacingContent === $(document.getElementById(id)).text(); + }); + clean(); + return Boolean(result); } ); }); From b992bcc7322617fd215ce29a3f67283b89a897e5 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 11 Feb 2017 10:40:34 +0100 Subject: [PATCH 08/79] added test for setMessage function, fixing bug for elements with only one child --- js/privatebin.js | 2 +- js/test.js | 52 +++++++++++++++++++++++++++++++++++++++++++++-- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 4fd0e99b..8b98ba8b 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -136,7 +136,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { setMessage: function(element, message) { var content = element.contents(); - if (content.length > 0) + if (content.length > 1) { content[content.length - 1].nodeValue = ' ' + message; } diff --git a/js/test.js b/js/test.js index 93e7a8c1..7906743c 100644 --- a/js/test.js +++ b/js/test.js @@ -77,7 +77,7 @@ describe('helper', function () { var html = '', result = true; ids.forEach(function(item, i) { - html += '
' + (contents[i] || contents[0]) + '
'; + html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; }); var clean = jsdom(html); ids.forEach(function(item, i) { @@ -102,7 +102,7 @@ describe('helper', function () { var html = '', result = true; ids.forEach(function(item, i) { - html += '
' + (contents[i] || contents[0]) + '
'; + html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; }); var clean = jsdom(html); ids.forEach(function(item, i) { @@ -117,6 +117,54 @@ describe('helper', function () { ); }); + describe('setMessage', function () { + jsc.property( + 'replaces the content of an empty element, analog to setElementText', + jsc.nearray(jsc.nearray(jsc.elements(alnumString))), + 'nearray string', + 'string', + function (ids, contents, replacingContent) { + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + ids.forEach(function(item, i) { + var id = item.join(''), + element = $(document.getElementById(id)); + $.PrivateBin.helper.setMessage(element, replacingContent); + result *= replacingContent === $(document.getElementById(id)).text(); + }); + clean(); + return Boolean(result); + } + ); + jsc.property( + 'replaces the last child of a non-empty element', + jsc.nearray(jsc.nearray(jsc.elements(alnumString))), + jsc.nearray(jsc.nearray(jsc.elements(alnumString))), + 'nearray string', + 'string', + function (ids, classes, contents, replacingContent) { + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + ids.forEach(function(item, i) { + var id = item.join(''), + element = $(document.getElementById(id)); + $.PrivateBin.helper.setMessage(element, replacingContent); + result *= ' ' + replacingContent === $(document.getElementById(id)).contents()[1].nodeValue; + }); + clean(); + return Boolean(result); + } + ); + }); + describe('scriptLocation', function () { jsc.property( 'returns the URL without query & fragment', diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 60c6727d..50f203bd 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..88af3aa9 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From b00bcd1352e1726a3f48309200b79bd9841084a4 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 11 Feb 2017 16:02:24 +0100 Subject: [PATCH 09/79] added test for urls2links function, fixing bug - asterisk is allowed in URLs query string --- js/privatebin.js | 4 ++-- js/test.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++ tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 8b98ba8b..e5067f47 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -164,13 +164,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var markup = '$1'; element.html( element.html().replace( - /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, + /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, markup ) ); element.html( element.html().replace( - /((magnet):[\w?=&.\/-;#@~%+-]+)/ig, + /((magnet):[\w?=&.\/-;#@~%+*-]+)/ig, markup ) ); diff --git a/js/test.js b/js/test.js index 7906743c..09ba8813 100644 --- a/js/test.js +++ b/js/test.js @@ -165,6 +165,64 @@ describe('helper', function () { ); }); + describe('urls2links', function () { + jsc.property( + 'ignores non-URL content', + 'string', + function (content) { + var element = $('
' + content + '
'), + before = element.html(); + $.PrivateBin.helper.urls2links(element); + return before === element.html(); + } + ); + jsc.property( + 'replaces URLs with anchors', + 'string', + jsc.elements(['http', 'https', 'ftp']), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + jsc.array(jsc.elements(queryString)), + 'string', + function (prefix, schema, address, query, fragment, postfix) { + var query = query.join(''), + fragment = fragment.join(''), + url = schema + '://' + address.join('') + '/?' + query + '#' + fragment, + prefix = $.PrivateBin.helper.htmlEntities(prefix), + postfix = ' ' + $.PrivateBin.helper.htmlEntities(postfix), + element = $('
' + prefix + url + postfix + '
'); + + // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. � or &#x + if ( + query.slice(-1) === '&' && + (parseInt(fragment.substring(0, 1), 10) >= 0 || fragment.charAt(0) === 'x' ) + ) + { + url = schema + '://' + address.join('') + '/?' + query.substring(0, query.length - 1); + postfix = ''; + element = $('
' + prefix + url + '
'); + } + + $.PrivateBin.helper.urls2links(element); + return element.html() === $('
' + prefix + '' + url + '' + postfix + '
').html(); + } + ); + jsc.property( + 'replaces magnet links with anchors', + 'string', + jsc.array(jsc.elements(queryString)), + 'string', + function (prefix, query, postfix) { + var url = 'magnet:?' + query.join(''), + prefix = $.PrivateBin.helper.htmlEntities(prefix), + postfix = $.PrivateBin.helper.htmlEntities(postfix), + element = $('
' + prefix + url + ' ' + postfix + '
'); + $.PrivateBin.helper.urls2links(element); + return element.html() === $('
' + prefix + '' + url + ' ' + postfix + '
').html(); + } + ); + }); + describe('scriptLocation', function () { jsc.property( 'returns the URL without query & fragment', diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 50f203bd..15f83e09 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 88af3aa9..ccfeacb1 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From b9c05b06d0aa318ac8dd49c1c7f405d37abc3135 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 11 Feb 2017 19:34:51 +0100 Subject: [PATCH 10/79] added test for sprintf function, removing dead code and optimizing test cases --- js/privatebin.js | 36 ++++++++-------- js/test.js | 104 ++++++++++++++++++++++++++++++++++++++++------ tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 111 insertions(+), 33 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index e5067f47..bb742ea8 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -179,6 +179,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * minimal sprintf emulation for %s and %d formats * + * Note that this function needs the parameters in the same order as the + * format strings appear in the string, contrary to the original. + * * @see {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914} * @name helper.sprintf * @function @@ -195,27 +198,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } var format = args[0], i = 1; - return format.replace(/%((%)|s|d)/g, function (m) { + 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; + var 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; }); }, diff --git a/js/test.js b/js/test.js index 09ba8813..cf44a19c 100644 --- a/js/test.js +++ b/js/test.js @@ -93,6 +93,10 @@ describe('helper', function () { }); describe('setElementText', function () { + after(function () { + cleanup(); + }); + jsc.property( 'replaces the content of an element', jsc.nearray(jsc.nearray(jsc.elements(alnumString))), @@ -104,20 +108,23 @@ describe('helper', function () { ids.forEach(function(item, i) { html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; }); - var clean = jsdom(html); + var elements = $('').html(html); ids.forEach(function(item, i) { var id = item.join(''), - element = $(document.getElementById(id)); + element = elements.find('#' + id).first(); $.PrivateBin.helper.setElementText(element, replacingContent); - result *= replacingContent === $(document.getElementById(id)).text(); + result *= replacingContent === element.text(); }); - clean(); return Boolean(result); } ); }); describe('setMessage', function () { + after(function () { + cleanup(); + }); + jsc.property( 'replaces the content of an empty element, analog to setElementText', jsc.nearray(jsc.nearray(jsc.elements(alnumString))), @@ -129,14 +136,13 @@ describe('helper', function () { ids.forEach(function(item, i) { html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; }); - var clean = jsdom(html); + var elements = $('').html(html); ids.forEach(function(item, i) { var id = item.join(''), - element = $(document.getElementById(id)); + element = elements.find('#' + id).first(); $.PrivateBin.helper.setMessage(element, replacingContent); - result *= replacingContent === $(document.getElementById(id)).text(); + result *= replacingContent === element.text(); }); - clean(); return Boolean(result); } ); @@ -152,20 +158,23 @@ describe('helper', function () { ids.forEach(function(item, i) { html += '
' + $.PrivateBin.helper.htmlEntities(contents[i] || contents[0]) + '
'; }); - var clean = jsdom(html); + var elements = $('').html(html); ids.forEach(function(item, i) { var id = item.join(''), - element = $(document.getElementById(id)); + element = elements.find('#' + id).first(); $.PrivateBin.helper.setMessage(element, replacingContent); - result *= ' ' + replacingContent === $(document.getElementById(id)).contents()[1].nodeValue; + result *= ' ' + replacingContent === element.contents()[1].nodeValue; }); - clean(); return Boolean(result); } ); }); describe('urls2links', function () { + after(function () { + cleanup(); + }); + jsc.property( 'ignores non-URL content', 'string', @@ -223,6 +232,77 @@ describe('helper', function () { ); }); + describe('sprintf', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'replaces %s in strings with first given parameter', + 'string', + '(small nearray) string', + 'string', + function (prefix, params, postfix) { + params.unshift(prefix + '%s' + postfix); + var result = prefix + params[1] + postfix; + return result === $.PrivateBin.helper.sprintf.apply(this, params) && + result === $.PrivateBin.helper.sprintf(params); + } + ); + jsc.property( + 'replaces %d in strings with first given parameter', + 'string', + '(small nearray) nat', + 'string', + function (prefix, params, postfix) { + params.unshift(prefix + '%d' + postfix); + var result = prefix + params[1] + postfix; + return result === $.PrivateBin.helper.sprintf.apply(this, params) && + result === $.PrivateBin.helper.sprintf(params); + } + ); + jsc.property( + 'replaces %d in strings with 0 if first parameter is not a number', + 'string', + '(small nearray) falsy', + 'string', + function (prefix, params, postfix) { + params.unshift(prefix + '%d' + postfix); + var result = prefix + '0' + postfix; + return result === $.PrivateBin.helper.sprintf.apply(this, params) && + result === $.PrivateBin.helper.sprintf(params); + } + ); + jsc.property( + 'replaces %d and %s in strings in order', + 'string', + 'nat', + 'string', + 'string', + 'string', + function (prefix, uint, middle, string, postfix) { + var params = [prefix + '%d' + middle + '%s' + postfix, uint, string], + result = prefix + uint + middle + string + postfix; + return result === $.PrivateBin.helper.sprintf.apply(this, params) && + result === $.PrivateBin.helper.sprintf(params); + } + ); + jsc.property( + 'replaces %d and %s in strings in reverse order', + 'string', + 'nat', + 'string', + 'string', + 'string', + function (prefix, uint, middle, string, postfix) { + var params = [prefix + '%s' + middle + '%d' + postfix, string, uint], + result = prefix + string + middle + uint + postfix; + return result === $.PrivateBin.helper.sprintf.apply(this, params) && + result === $.PrivateBin.helper.sprintf(params); + } + ); + }); + describe('scriptLocation', function () { jsc.property( 'returns the URL without query & fragment', diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 15f83e09..7f8c25a1 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 ccfeacb1..f2f8ef7a 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From ccac5d373beba7265b261326b50e22ab664f4c1f Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 14:30:41 +0100 Subject: [PATCH 11/79] working on integrating mocha into travis CI --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index c5e0d074..77b6bbd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,14 @@ php: before_script: - composer install -n + - npm install -g mocha + - cd js + - npm install jsverify jsdom jsdom-global + - cd .. script: - cd tst && phpunit + - cd ../js && mocha after_script: - cd .. From 9d246d4abd8bf66c50aa027325f03a964b996d23 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 14:50:06 +0100 Subject: [PATCH 12/79] trying to convince travis CI to use node.js 4 or later --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 77b6bbd9..d034ad95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ php: - 5.5 - 5.6 - 7.0 +node_js: + - "node" before_script: - composer install -n From 7163ac40b52f6f62b8873e63c673c01114584bc7 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 14:57:20 +0100 Subject: [PATCH 13/79] trying harder to convince travis CI to use node.js 4 --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d034ad95..11575cf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,12 @@ php: - 5.5 - 5.6 - 7.0 -node_js: - - "node" + +env: + - TRAVIS_NODE_VERSION="4" + +install: + - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION before_script: - composer install -n From 0a6fd87b4c9541e57dbf36c21b6a1e7170872bae Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 15:02:29 +0100 Subject: [PATCH 14/79] simplify travis CI configuration --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 11575cf2..e8ec3f2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,8 @@ php: - 5.6 - 7.0 -env: - - TRAVIS_NODE_VERSION="4" - install: - - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION + - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 4 before_script: - composer install -n From 0c5148a790723cfa63e97ce45909d1dc83d43227 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 15:03:08 +0100 Subject: [PATCH 15/79] updating tested PHP versions in travis CI configuration --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e8ec3f2f..0185aa31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ language: php sudo: false php: - - 5.5 - - 5.6 - - 7.0 + - '5.4' + - '5.5' + - '5.6' + - '7.0' + - '7.1' install: - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 4 From 1457b5ec6ab9ca5fb3564b15af6067c608af8f11 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 15:16:29 +0100 Subject: [PATCH 16/79] ensuring tests will work with PHP 5.4, the currently oldest supported version --- tst/Data/DatabaseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tst/Data/DatabaseTest.php b/tst/Data/DatabaseTest.php index 64e9c064..4e06586c 100644 --- a/tst/Data/DatabaseTest.php +++ b/tst/Data/DatabaseTest.php @@ -311,7 +311,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase 'vizhash BLOB, ' . 'postdate INT );' ); - $this->assertInstanceOf(Database::class, Database::getInstance($this->_options)); + $this->assertInstanceOf('PrivateBin\\Data\\Database', Database::getInstance($this->_options)); // check if version number was upgraded in created configuration table $statement = $db->prepare('SELECT value FROM foo_config WHERE id LIKE ?'); From b1396a249d193e1cfe9cb45519e2fefe4694a08b Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 15:30:11 +0100 Subject: [PATCH 17/79] ensuring that in the JS sprintf tests no replacable patterns occur in the pre- & postfix of the test string --- js/test.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/js/test.js b/js/test.js index cf44a19c..b5845ae4 100644 --- a/js/test.js +++ b/js/test.js @@ -243,8 +243,10 @@ describe('helper', function () { '(small nearray) string', 'string', function (prefix, params, postfix) { + var prefix = prefix.replace(/%(s|d)/g, '%%'), + postfix = postfix.replace(/%(s|d)/g, '%%'), + result = prefix + params[0] + postfix; params.unshift(prefix + '%s' + postfix); - var result = prefix + params[1] + postfix; return result === $.PrivateBin.helper.sprintf.apply(this, params) && result === $.PrivateBin.helper.sprintf(params); } @@ -255,8 +257,10 @@ describe('helper', function () { '(small nearray) nat', 'string', function (prefix, params, postfix) { + var prefix = prefix.replace(/%(s|d)/g, '%%'), + postfix = postfix.replace(/%(s|d)/g, '%%'), + result = prefix + params[0] + postfix; params.unshift(prefix + '%d' + postfix); - var result = prefix + params[1] + postfix; return result === $.PrivateBin.helper.sprintf.apply(this, params) && result === $.PrivateBin.helper.sprintf(params); } @@ -267,8 +271,10 @@ describe('helper', function () { '(small nearray) falsy', 'string', function (prefix, params, postfix) { + var prefix = prefix.replace(/%(s|d)/g, '%%'), + postfix = postfix.replace(/%(s|d)/g, '%%'), + result = prefix + '0' + postfix; params.unshift(prefix + '%d' + postfix); - var result = prefix + '0' + postfix; return result === $.PrivateBin.helper.sprintf.apply(this, params) && result === $.PrivateBin.helper.sprintf(params); } @@ -281,7 +287,9 @@ describe('helper', function () { 'string', 'string', function (prefix, uint, middle, string, postfix) { - var params = [prefix + '%d' + middle + '%s' + postfix, uint, string], + var prefix = prefix.replace(/%(s|d)/g, '%%'), + postfix = postfix.replace(/%(s|d)/g, '%%'), + params = [prefix + '%d' + middle + '%s' + postfix, uint, string], result = prefix + uint + middle + string + postfix; return result === $.PrivateBin.helper.sprintf.apply(this, params) && result === $.PrivateBin.helper.sprintf(params); @@ -295,7 +303,9 @@ describe('helper', function () { 'string', 'string', function (prefix, uint, middle, string, postfix) { - var params = [prefix + '%s' + middle + '%d' + postfix, string, uint], + var prefix = prefix.replace(/%(s|d)/g, '%%'), + postfix = postfix.replace(/%(s|d)/g, '%%'), + params = [prefix + '%s' + middle + '%d' + postfix, string, uint], result = prefix + string + middle + uint + postfix; return result === $.PrivateBin.helper.sprintf.apply(this, params) && result === $.PrivateBin.helper.sprintf(params); From eedb05111ad9c79aef6f1972c760372c7e3a2a50 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 12 Feb 2017 17:11:21 +0100 Subject: [PATCH 18/79] added test for getCookie function, documenting its limitation of not finding cookies with empty identifier --- js/privatebin.js | 2 +- js/test.js | 27 +++++++++++++++++++++++++++ tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index bb742ea8..34a00f12 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -224,7 +224,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @see {@link http://www.w3schools.com/js/js_cookies.asp} * @name helper.getCookie * @function - * @param {string} cname + * @param {string} cname - may not be empty * @return {string} */ getCookie: function(cname) { diff --git a/js/test.js b/js/test.js index b5845ae4..68792069 100644 --- a/js/test.js +++ b/js/test.js @@ -313,6 +313,33 @@ describe('helper', function () { ); }); + describe('getCookie', function () { + jsc.property( + 'returns the requested cookie', + 'nearray asciinestring', + 'nearray asciistring', + function (labels, values) { + var selectedKey = '', selectedValue = '', + cookieArray = [], + count = 0; + labels.forEach(function(item, i) { + var key = item.replace(/[\s;,=]/g, 'x'), + value = (values[i] || values[0]).replace(/[\s;,=]/g, ''); + cookieArray.push(key + '=' + value); + if (Math.random() < 1 / i) + { + selectedKey = key; + selectedValue = value; + } + }); + var clean = jsdom('', {cookie: cookieArray}), + result = $.PrivateBin.helper.getCookie(selectedKey); + clean(); + return result === selectedValue; + } + ); + }); + describe('scriptLocation', function () { jsc.property( 'returns the URL without query & fragment', diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 7f8c25a1..5f5fe6a7 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 f2f8ef7a..3bd4b530 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 19/79] 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 38/79] 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 39/79] 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 52/79] 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 53/79] 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 54/79] 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 55/79] 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 56/79] 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 57/79] 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 58/79] 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 59/79] 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 60/79] 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 61/79] 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 62/79] 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 63/79] 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 64/79] 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 65/79] 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 66/79] 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 67/79] 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 68/79] 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 69/79] 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 70/79] 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 71/79] 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): - + From e80c726f92c3694e7f60e58fec472e3070eddf9d Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 25 Mar 2017 12:46:08 +0100 Subject: [PATCH 72/79] added unit test for missing message IDs accross all translations, added IDs found this way to translation files (#201) --- i18n/de.json | 3 +-- i18n/es.json | 13 ++++++++----- i18n/fr.json | 13 ++++++++----- i18n/it.json | 13 ++++++++----- i18n/no.json | 13 ++++++++----- i18n/oc.json | 13 ++++++++----- i18n/pl.json | 13 ++++++++----- i18n/pt.json | 5 ++++- i18n/ru.json | 10 ++++------ i18n/sl.json | 13 ++++++++----- tst/I18nTest.php | 20 ++++++++++++++++++++ 11 files changed, 85 insertions(+), 44 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index 7974d773..332c61c8 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -151,6 +151,5 @@ "Preparing new paste…": "Bereite neues Paste vor…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in die FAQ (englisch), um zu sehen, wie der Fehler behoben werden kann.", - "+++ no paste text +++": - "+++ kein Paste-Text +++" + "+++ no paste text +++": "+++ kein Paste-Text +++" } diff --git a/i18n/es.json b/i18n/es.json index e957c8f8..98251e98 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -92,7 +92,7 @@ "Responder", "Anonymous": "Anónimo", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Avatar anónimo (Vizhash de la dirección IP)", "Add comment": "Añadir comentario", @@ -112,8 +112,8 @@ "Error del servidor o el servidor no responde", "Could not post comment: %s": "No fue posible publicar comentario: %s", - "Sending paste (Please move your mouse for more entropy)…": - "Enviando texto (Por favor, mueva el ratón para mayor entropía)…", + "Please move your mouse for more entropy…": + "Por favor, mueva el ratón para mayor entropía…", "Sending paste…": "Enviando texto…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,7 @@ "Source Code": "Código fuente", "Markdown": "Markdown", "Download attachment": "Descargar adjunto", - "Cloned file attached.": "Archivo clonado adjunto.", + "Cloned: '%s'": "Clonado: '%s'.", "Attach a file": "Adjuntar archivo", "Remove attachment": "Remover adjunto", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -146,6 +146,9 @@ "Enter password": "Ingrese contraseña", "Loading…": "Cargando…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "En caso de que este mensaje nunca desaparezca por favor revise este FAQ para obtener información para solucionar problemas." + "En caso de que este mensaje nunca desaparezca por favor revise este FAQ para obtener información para solucionar problemas.", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/fr.json b/i18n/fr.json index 35cfd659..d5d447e0 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -92,7 +92,7 @@ "Répondre", "Anonymous": "Anonyme", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Avatar anonyme (Vizhash de l'adresse IP)", "Add comment": "Ajouter un commentaire", @@ -112,8 +112,8 @@ "Le serveur ne répond pas ou a rencontré une erreur", "Could not post comment: %s": "Impossible de poster le commentaire : %s", - "Sending paste (Please move your mouse for more entropy)…": - "Envoi du paste (Merci de bouger votre souris pour plus d'entropie)…", + "Please move your mouse for more entropy…": + "Merci de bouger votre souris pour plus d'entropie…", "Sending paste…": "Envoi du paste…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -138,7 +138,7 @@ "Source Code": "Code source", "Markdown": "Markdown", "Download attachment": "Télécharger la pièce jointe", - "Cloned file attached.": "Cloner le fichier attaché.", + "Cloned: '%s'": "Cloner '%s'", "Attach a file": "Attacher un fichier ", "Remove attachment": "Enlever l'attachement", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -155,6 +155,9 @@ "Enter password": "Entrez le mot de passe", "Loading…": "Chargement…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Si ce message ne disparaîssait pas, jetez un oeil à cette FAQ pour des idées de résolution (en Anglais)." + "Si ce message ne disparaîssait pas, jetez un oeil à cette FAQ pour des idées de résolution (en Anglais).", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/it.json b/i18n/it.json index dc56fa75..b7b19e8c 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -92,7 +92,7 @@ "Rispondi", "Anonymous": "Anonimo", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Avatar Anonimo (Vizhash dell'indirizzo IP)", "Add comment": "Aggiungi un commento", @@ -112,8 +112,8 @@ "errore o mancata risposta dal server", "Could not post comment: %s": "Impossibile inviare il commento: %s", - "Sending paste (Please move your mouse for more entropy)…": - "Invio messaggio (Muovi il mouse in modo casuale, per generare maggior entropia)…", + "Please move your mouse for more entropy…": + "Muovi il mouse in modo casuale, per generare maggior entropia…", "Sending paste…": "Messaggio in fase di invio…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,7 @@ "Source Code": "Codice Sorgente", "Markdown": "Markdown", "Download attachment": "Scarica Allegato", - "Cloned file attached.": "Copia del file allegata.", + "Cloned: '%s'": "Copia: '%s'", "Attach a file": "Allega un file", "Remove attachment": "Rimuovi allegato", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -146,6 +146,9 @@ "Enter password": "Inserisci la password", "Loading…": "Loading…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema (in Inglese)." + "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema (in Inglese).", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/no.json b/i18n/no.json index fd56583d..f131e4fe 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -92,7 +92,7 @@ "Svar", "Anonymous": "Anonym", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Anonym avatar (Vizhash av IP adressen)", "Add comment": "Legg til kommentar", @@ -112,8 +112,8 @@ "server feilet eller svarer ikke", "Could not post comment: %s": "Kunne ikke sende kommentar: %s", - "Sending paste (Please move your mouse for more entropy)…": - "Sender innlegg (Flytt musen for mere entropi)…", + "Please move your mouse for more entropy…": + "Flytt musen for mere entropi…", "Sending paste…": "Sender innlegg…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,7 @@ "Source Code": "Kildekode", "Markdown": "Oppmerket", "Download attachment": "Last ned vedlegg", - "Cloned file attached.": "Kopier vedlegg.", + "Cloned: '%s'": "Kopier: '%s'", "Attach a file": "Legg til fil", "Remove attachment": "Slett vedlegg", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -146,6 +146,9 @@ "Enter password": "Skriv inn passord", "Loading…": "Laster…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med ofte stilte spørsmål for informasjon om feilsøking." + "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med ofte stilte spørsmål for informasjon om feilsøking.", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/oc.json b/i18n/oc.json index b6b449de..8f923b1b 100644 --- a/i18n/oc.json +++ b/i18n/oc.json @@ -92,7 +92,7 @@ "Respondre", "Anonymous": "Anonime", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Avatar anonime (Vizhash de l'adreça IP)", "Add comment": "Apondre un comentari", @@ -112,8 +112,8 @@ "Lo servidor respond pas o a rencontrat una error", "Could not post comment: %s": "Impossible de mandar lo comentari : %s", - "Sending paste (Please move your mouse for more entropy)…": - "Mandadís del tèxte (Mercés de bolegar vòstra mirga per mai entropia)…", + "Please move your mouse for more entropy…": + "Mercés de bolegar vòstra mirga per mai entropia…", "Sending paste…": "Mandadís del tèxte…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -138,7 +138,7 @@ "Source Code": "Còdi font", "Markdown": "Markdown", "Download attachment": "Telecargar la pèça junta", - "Cloned file attached.": "Clonar lo fichièr junt.", + "Cloned: '%s'": "Clonar: '%s'", "Attach a file": "Juntar un fichièr ", "Remove attachment": "Levar la pèca junta", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -155,6 +155,9 @@ "Enter password": "Picatz lo senhal", "Loading…": "Cargament…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar aquesta FAQ per las solucions (en Anglés)." + "Se per cas aqueste messatge quita pas de s'afichar mercés de gaitar aquesta FAQ per las solucions (en Anglés).", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/pl.json b/i18n/pl.json index 99554981..b722c480 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -92,7 +92,7 @@ "Odpowiedz", "Anonymous": "Anonim", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Anonimowy avatar (Vizhash z adresu IP)", "Add comment": "Dodaj komentarz", @@ -112,8 +112,8 @@ "bląd serwera lub brak odpowiedzi", "Could not post comment: %s": "Nie udało się wysłać komentarza: %s", - "Sending paste (Please move your mouse for more entropy)…": - "Wysyłanie wklejki (proszę poruszać myszą aby uzyskać większą entropię)…", + "Please move your mouse for more entropy…": + "Proszę poruszać myszą aby uzyskać większą entropię…", "Sending paste…": "Wysyłanie wklejki…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,7 @@ "Source Code": "Kod źródłowy", "Markdown": "Markdown", "Download attachment": "Pobierz załącznik", - "Cloned file attached.": "Sklonowano załączony plik.", + "Cloned: '%s'": "Sklonowano: '%s'", "Attach a file": "Załącz plik", "Remove attachment": "Usuń załącznik", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -146,6 +146,9 @@ "Enter password": "Wpisz hasło", "Loading…": "Loading…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "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)." + "In case this message never disappears please have a look at this FAQ for information to troubleshoot (in English).", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/pt.json b/i18n/pt.json index 06f72c51..01d5dfe8 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -146,6 +146,9 @@ "Enter password": "Digite a senha", "Loading…": "Carregando…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Caso essa mensagem nunca desapareça, por favor veja este FAQ para saber como resolver os problemas." + "Caso essa mensagem nunca desapareça, por favor veja este FAQ para saber como resolver os problemas.", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/i18n/ru.json b/i18n/ru.json index ca4c707b..53431ad2 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -92,7 +92,7 @@ "Ответить", "Anonymous": "Аноним", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Анонимный аватар (Vizhash IP адреса)", "Add comment": "Добавить комментарий", @@ -104,6 +104,8 @@ "Отправка комментария…", "Comment posted.": "Комментарий опубликован.", + "Could not refresh display: %s": + "Could not refresh display: %s", "unknown status": "неизвестная причина", "server error or not responding": @@ -138,7 +140,6 @@ "Source Code": "Исходный код", "Markdown": "Язык разметки", "Download attachment": "Скачать прикрепленный файл", - "Cloned file attached.": "Дубликат файла прикреплен.", "Cloned: '%s'": "Дублировано: '%s'", "The cloned file '%s' was attached to this paste.": "Дубликат файла '%s' был прикреплен к этой записи.", @@ -157,13 +158,10 @@ "Расшифровать", "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 +++": - "+++ в записи нет текста +++" + "+++ no paste text +++": "+++ в записи нет текста +++" } diff --git a/i18n/sl.json b/i18n/sl.json index 00b09702..4c0a7e9f 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -92,7 +92,7 @@ "Odgovori", "Anonymous": "Aninomno", - "Anonymous avatar (Vizhash of the IP address)": + "Avatar generated from IP address": "Anonimen avatar (Vizhash IP naslova)", "Add comment": "Dodaj komentar", @@ -112,8 +112,8 @@ "napaka na strežniku, ali pa se strežnik ne odziva", "Could not post comment: %s": "Komentarja ni bilo mogoče objaviti : %s", - "Sending paste (Please move your mouse for more entropy)…": - "Pošiljam prilepek (prosim premakni svojo miško za več entropije) …", + "Please move your mouse for more entropy…": + "Prosim premakni svojo miško za več entropije…", "Sending paste…": "Pošiljam prilepek…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -138,7 +138,7 @@ "Source Code": "Odprta koda", "Markdown": "Markdown", "Download attachment": "Pretoči priponko", - "Cloned file attached.": "Pripeta datoteka klonirana", + "Cloned: '%s'": "'%s' klonirana", "Attach a file": "Pripni datoteko", "Remove attachment": "Odstrani priponko", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -155,6 +155,9 @@ "Enter password": "Prosim vnesi geslo", "Loading…": "Loading…", + "Decrypting paste…": "Decrypting paste…", + "Preparing new paste…": "Preparing new paste…", "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)." + "In case this message never disappears please have a look at this FAQ for information to troubleshoot (in English).", + "+++ no paste text +++": "+++ no paste text +++" } diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 91c92aa6..187fd1a2 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -142,4 +142,24 @@ class I18nTest extends PHPUnit_Framework_TestCase I18n::loadTranslations(); $this->assertEquals('some string + 1', I18n::_('some %s + %d', 'string', 1), 'browser language en'); } + + public function testMessageIdsExistInAllLanguages() + { + $messageIds = array(); + $languages = array(); + $dir = dir(PATH . 'i18n'); + while (false !== ($file = $dir->read())) { + if (strlen($file) === 7) { + $language = substr($file, 0, 2); + $translations = json_decode(file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file), true); + $messageIds = array_unique($messageIds + array_keys($translations)); + $languages[$language] = $translations; + } + } + foreach ($messageIds as $messageId) { + foreach (array_keys($languages) as $language) { + $this->assertArrayHasKey($messageId, $languages[$language], "message ID '$messageId' exists in translation file $language.json"); + } + } + } } From 44327bed58b409500fdb62b37efabadc1309a4ba Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 25 Mar 2017 13:19:11 +0100 Subject: [PATCH 73/79] added missing/removed translation IDs found using improved unit test (#201) --- i18n/es.json | 1 + i18n/fr.json | 1 + i18n/it.json | 1 + i18n/no.json | 1 + i18n/oc.json | 1 + i18n/pl.json | 1 + i18n/pt.json | 1 + i18n/ru.json | 2 -- i18n/sl.json | 1 + tst/I18nTest.php | 26 +++++++++++++++++++------- 10 files changed, 27 insertions(+), 9 deletions(-) diff --git a/i18n/es.json b/i18n/es.json index 98251e98..427f90dc 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -130,6 +130,7 @@ "Markdown": "Markdown", "Download attachment": "Descargar adjunto", "Cloned: '%s'": "Clonado: '%s'.", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Adjuntar archivo", "Remove attachment": "Remover adjunto", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/fr.json b/i18n/fr.json index d5d447e0..10c36a4e 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -139,6 +139,7 @@ "Markdown": "Markdown", "Download attachment": "Télécharger la pièce jointe", "Cloned: '%s'": "Cloner '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Attacher un fichier ", "Remove attachment": "Enlever l'attachement", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/it.json b/i18n/it.json index b7b19e8c..583e0e49 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -130,6 +130,7 @@ "Markdown": "Markdown", "Download attachment": "Scarica Allegato", "Cloned: '%s'": "Copia: '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Allega un file", "Remove attachment": "Rimuovi allegato", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/no.json b/i18n/no.json index f131e4fe..c0d376c5 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -130,6 +130,7 @@ "Markdown": "Oppmerket", "Download attachment": "Last ned vedlegg", "Cloned: '%s'": "Kopier: '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Legg til fil", "Remove attachment": "Slett vedlegg", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/oc.json b/i18n/oc.json index 8f923b1b..90478768 100644 --- a/i18n/oc.json +++ b/i18n/oc.json @@ -139,6 +139,7 @@ "Markdown": "Markdown", "Download attachment": "Telecargar la pèça junta", "Cloned: '%s'": "Clonar: '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Juntar un fichièr ", "Remove attachment": "Levar la pèca junta", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/pl.json b/i18n/pl.json index b722c480..82d9b579 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -130,6 +130,7 @@ "Markdown": "Markdown", "Download attachment": "Pobierz załącznik", "Cloned: '%s'": "Sklonowano: '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Załącz plik", "Remove attachment": "Usuń załącznik", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/pt.json b/i18n/pt.json index 01d5dfe8..6af67cf6 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -130,6 +130,7 @@ "Markdown": "Markdown", "Download attachment": "Baixar anexo", "Cloned: '%s'": "Clonado: '%s'", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Anexar um arquivo", "Remove attachment": "Remover anexo", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/i18n/ru.json b/i18n/ru.json index 53431ad2..989d3e4f 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -110,8 +110,6 @@ "неизвестная причина", "server error or not responding": "ошибка сервера или нет ответа", - "unknown error": - "неизвестная ошибка", "Could not post comment: %s": "Не удалось опубликовать комментарий: %s", "Please move your mouse for more entropy…": diff --git a/i18n/sl.json b/i18n/sl.json index 4c0a7e9f..21db8c1d 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -139,6 +139,7 @@ "Markdown": "Markdown", "Download attachment": "Pretoči priponko", "Cloned: '%s'": "'%s' klonirana", + "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Pripni datoteko", "Remove attachment": "Odstrani priponko", "Your browser does not support uploading encrypted files. Please use a newer browser.": diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 187fd1a2..c7ded0ee 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -146,19 +146,31 @@ class I18nTest extends PHPUnit_Framework_TestCase public function testMessageIdsExistInAllLanguages() { $messageIds = array(); - $languages = array(); - $dir = dir(PATH . 'i18n'); + $languages = array(); + $dir = dir(PATH . 'i18n'); while (false !== ($file = $dir->read())) { if (strlen($file) === 7) { - $language = substr($file, 0, 2); - $translations = json_decode(file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file), true); - $messageIds = array_unique($messageIds + array_keys($translations)); - $languages[$language] = $translations; + $language = substr($file, 0, 2); + $languageMessageIds = array_keys( + json_decode( + file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file), + true + ) + ); + $messageIds = array_unique(array_merge($messageIds, $languageMessageIds)); + $languages[$language] = $languageMessageIds; } } foreach ($messageIds as $messageId) { foreach (array_keys($languages) as $language) { - $this->assertArrayHasKey($messageId, $languages[$language], "message ID '$messageId' exists in translation file $language.json"); + // most languages don't translate the data size units, ignore those + if ($messageId !== 'B' && strlen($messageId) !== 3 && strpos($messageId, 'B', 2) !== 2) { + $this->assertContains( + $messageId, + $languages[$language], + "message ID '$messageId' exists in translation file $language.json" + ); + } } } } From 3fc6ede5bbc32d61826d53ddfe0b478c474148b2 Mon Sep 17 00:00:00 2001 From: Tulio Leao Date: Sat, 25 Mar 2017 09:26:31 -0300 Subject: [PATCH 74/79] Translate missing IDs to Portuguese As referenced on issue #201, some IDs were missing from the pt translation, which were now translated. --- i18n/pt.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/pt.json b/i18n/pt.json index 6af67cf6..05ce23d1 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -130,7 +130,7 @@ "Markdown": "Markdown", "Download attachment": "Baixar anexo", "Cloned: '%s'": "Clonado: '%s'", - "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", + "The cloned file '%s' was attached to this paste.": "O arquivo clonado '%s' foi anexado a essa cópia.", "Attach a file": "Anexar um arquivo", "Remove attachment": "Remover anexo", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -147,9 +147,9 @@ "Enter password": "Digite a senha", "Loading…": "Carregando…", - "Decrypting paste…": "Decrypting paste…", - "Preparing new paste…": "Preparing new paste…", + "Decrypting paste…": "Decifrando cópia…", + "Preparing new paste…": "Preparando nova cópia…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Caso essa mensagem nunca desapareça, por favor veja este FAQ para saber como resolver os problemas.", - "+++ no paste text +++": "+++ no paste text +++" + "+++ no paste text +++": "+++ sem texto de cópia +++" } From d8e0a6e986cb42d0d1040eecf6fc01e4bb1ccb2d Mon Sep 17 00:00:00 2001 From: Stefano Martinelli Date: Sat, 25 Mar 2017 14:42:31 +0100 Subject: [PATCH 75/79] Italian translation update - new strings translated - couple of minor errors fixed Some strings (i.e. line 133) are literal translations. I could probably give a better translation by reading the messages in their context, when I get the next update. --- i18n/it.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/i18n/it.json b/i18n/it.json index 583e0e49..e69ea81e 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -18,7 +18,7 @@ "Invalid data.": "Dati non validi.", "You are unlucky. Try again.": - "Riprova, sarai più fortunato.", + "Ritenta, sarai più fortunato.", "Error saving comment. Sorry.": "Errore durante il salvataggio del commento.", "Error saving paste. Sorry.": @@ -93,7 +93,7 @@ "Anonymous": "Anonimo", "Avatar generated from IP address": - "Avatar Anonimo (Vizhash dell'indirizzo IP)", + "Avatar generato dall'indirizzo IP)", "Add comment": "Aggiungi un commento", "Optional nickname…": @@ -107,7 +107,7 @@ "Could not refresh display: %s": "Non riesco ad aggiornare il display: %s", "unknown status": - "errore sconosciuto", + "stato sconosciuto", "server error or not responding": "errore o mancata risposta dal server", "Could not post comment: %s": @@ -117,11 +117,11 @@ "Sending paste…": "Messaggio in fase di invio…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": - "Il tuo messaggio è qui: %s ([CTRL | CMD]+[C] per copiare il link)", + "Il tuo messaggio è qui: %s (Premi [Ctrl]+[c] (Windows) o [Cmd]+[c] (Mac) per copiare il link)", "Delete data": "Cancella i dati", "Could not create paste: %s": - "Non rieco a creare il messaggio: %s", + "Non riesco a creare il messaggio: %s", "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Non riesco a decifrare il messaggio: manca la chiave di decifrazione nell'URL (La chiave è parte integrante dell'URL. Per caso hai usato un Redirector o un altro servizio che ha rimosso una parte dell'URL?)", "Format": "Formato", @@ -129,8 +129,8 @@ "Source Code": "Codice Sorgente", "Markdown": "Markdown", "Download attachment": "Scarica Allegato", - "Cloned: '%s'": "Copia: '%s'", - "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", + "Cloned: '%s'": "Clonato: '%s'", + "The cloned file '%s' was attached to this paste.": "Il file clonato '%s' era allegato a questo messaggio.", "Attach a file": "Allega un file", "Remove attachment": "Rimuovi allegato", "Your browser does not support uploading encrypted files. Please use a newer browser.": @@ -143,13 +143,13 @@ "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "%s necessita che PATH termini con \"%s\". Aggiorna la variabile PATH nel tuo index.php.", "Decrypt": - "Decrypt", + "Decifra", "Enter password": "Inserisci la password", - "Loading…": "Loading…", - "Decrypting paste…": "Decrypting paste…", - "Preparing new paste…": "Preparing new paste…", + "Loading…": "Carico…", + "Decrypting paste…": "Decifro il messaggio…", + "Preparing new paste…": "Preparo il nuovo messaggio…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema (in Inglese).", - "+++ no paste text +++": "+++ no paste text +++" + "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema.", + "+++ no paste text +++": "+++ nessun testo nel messaggio +++" } From 82e45915c73b566a1247c459fcbbd785c783d747 Mon Sep 17 00:00:00 2001 From: Alexey Pyltsyn Date: Sat, 25 Mar 2017 17:06:56 +0300 Subject: [PATCH 76/79] Update Russian translation --- i18n/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index 989d3e4f..da462c38 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -93,7 +93,7 @@ "Anonymous": "Аноним", "Avatar generated from IP address": - "Анонимный аватар (Vizhash IP адреса)", + "Аватар, сгенерированный из IP-адреса", "Add comment": "Добавить комментарий", "Optional nickname…": @@ -105,7 +105,7 @@ "Comment posted.": "Комментарий опубликован.", "Could not refresh display: %s": - "Could not refresh display: %s", + "Не удалось обновить отображение: %s", "unknown status": "неизвестная причина", "server error or not responding": From d23c696e97979d3a513ac991586f933f0de3bbd5 Mon Sep 17 00:00:00 2001 From: stefanomarty Date: Sat, 25 Mar 2017 17:33:54 +0100 Subject: [PATCH 77/79] Revert delete of (in Inglese) --- i18n/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/it.json b/i18n/it.json index e69ea81e..d7885d70 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -150,6 +150,6 @@ "Decrypting paste…": "Decifro il messaggio…", "Preparing new paste…": "Preparo il nuovo messaggio…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": - "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema.", + "Nel caso questo messaggio non scompaia, controlla questa FAQ per trovare informazioni su come risolvere il problema (in Inglese).", "+++ no paste text +++": "+++ nessun testo nel messaggio +++" } From cd40717301125d8a4cf638b81395b6f18733ccfc Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 25 Mar 2017 18:44:20 +0100 Subject: [PATCH 78/79] fixing #209, refactoring regression when file upload is disabled --- js/privatebin.js | 3 ++- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index c55a0271..65d81878 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2082,7 +2082,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.hasAttachment = function() { - return ($attachmentLink.prop('href') !== '') + var link = $attachmentLink.prop('href'); + return (typeof link !== 'undefined' && link !== '') } /** diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 5381863b..e1df5d7e 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 18d60e32..c38a1ebb 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -47,7 +47,7 @@ if ($MARKDOWN): - + From 806b665c6abb38018acd5047406af4e246d8f0e7 Mon Sep 17 00:00:00 2001 From: idarlund Date: Tue, 28 Mar 2017 16:42:48 +0200 Subject: [PATCH 79/79] Update no.json updated based on https://github.com/PrivateBin/PrivateBin/issues/201 --- i18n/no.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/i18n/no.json b/i18n/no.json index c0d376c5..8292e104 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -26,13 +26,13 @@ "Invalid paste ID.": "Feil innlegg ID.", "Paste is not of burn-after-reading type.": - "Innlegg er ikke av type slett-etter-lesing.", + "Innlegg er ikke av typen slett etter lesing.", "Wrong deletion token. Paste was not deleted.": "Feil slettingsnøkkel. Innlegg ble ikke fjernet.", "Paste was properly deleted.": "Innlegget er slettet.", "JavaScript is required for %s to work.
Sorry for the inconvenience.": - "Javascript kreves for at %s skal fungere
Beklager ulempene.", + "Javascript kreves for at %s skal fungere
Beklager.", "%s requires a modern browser to work.": "%s krever en moderne nettleser for å fungere.", "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": @@ -83,7 +83,7 @@ "Could not decrypt data (Wrong key?)": "Kunne ikke dekryptere data (Feil nøkkel?)", "Could not delete the paste, it was not stored in burn after reading mode.": - "Kan ikke slette innlegget, det ble ikke lagret i slett-etter-les modus.", + "Kan ikke slette innlegget, det ble ikke lagret som 'slett etter les' type.", "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "KUN FOR DINE ØYNE. Ikke lukk dette vinduet, denne meldingen kan ikke bli vist igjen.", "Could not decrypt comment; Wrong key?": @@ -93,7 +93,7 @@ "Anonymous": "Anonym", "Avatar generated from IP address": - "Anonym avatar (Vizhash av IP adressen)", + "Anonym avatar generert med data fra IP adressen)", "Add comment": "Legg til kommentar", "Optional nickname…": @@ -105,15 +105,15 @@ "Comment posted.": "Kommentar sendt.", "Could not refresh display: %s": - "Kunne ikke oppdatere skjermen: %s", + "Kunne ikke oppdatere bildet: %s", "unknown status": "ukjent status", "server error or not responding": - "server feilet eller svarer ikke", + "tjener feilet eller svarer ikke", "Could not post comment: %s": "Kunne ikke sende kommentar: %s", "Please move your mouse for more entropy…": - "Flytt musen for mere entropi…", + "Flytt musen for mer entropi…", "Sending paste…": "Sender innlegg…", "Your paste is %s (Hit [Ctrl]+[c] to copy)": @@ -129,7 +129,7 @@ "Source Code": "Kildekode", "Markdown": "Oppmerket", "Download attachment": "Last ned vedlegg", - "Cloned: '%s'": "Kopier: '%s'", + "Cloned: '%s'": "Kopiert: '%s'", "The cloned file '%s' was attached to this paste.": "The cloned file '%s' was attached to this paste.", "Attach a file": "Legg til fil", "Remove attachment": "Slett vedlegg", @@ -137,7 +137,7 @@ "Nettleseren din støtter ikke å laste opp krypterte filer. Vennligst bruk en nyere nettleser.", "Invalid attachment.": "Ugyldig vedlegg.", "Options": "Alternativer", - "Shorten URL": "Adresse-forkorter", + "Shorten URL": "Adresse forkorter", "Editor": "Rediger", "Preview": "Forhåndsvis", "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": @@ -147,9 +147,9 @@ "Enter password": "Skriv inn passord", "Loading…": "Laster…", - "Decrypting paste…": "Decrypting paste…", - "Preparing new paste…": "Preparing new paste…", + "Decrypting paste…": "Dekrypterer innlegg…", + "Preparing new paste…": "Klargjør nytt innlegg…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Hvis denne meldingen ikke forsvinner kan du ta en titt på siden med ofte stilte spørsmål for informasjon om feilsøking.", - "+++ no paste text +++": "+++ no paste text +++" + "+++ no paste text +++": "+++ ingen innleggstekst +++" }