diff --git a/i18n/de.json b/i18n/de.json index 87f55ccb..bab88a52 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -85,7 +85,7 @@ "Could not delete the paste, it was not stored in burn after reading mode.": "Konnte den Text nicht löschen, er wurde nicht im Einmal-Modus gespeichert.", "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": - "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schliesse das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.", + "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schließe das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.", "Could not decrypt comment; Wrong key?": "Konnte Kommentar nicht entschlüsseln; Falscher Schlüssel?", "Reply": @@ -147,5 +147,7 @@ "Passwort eingeben", "Loading…": "Lädt…", "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." + "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in die FAQ (englisch), um zu sehen, wie der Fehler behoben werden kann.", + "Nothing to see… Try to enter some text.": + "Nichts zu sehen… Versuche etwas Text einzugeben." } diff --git a/js/privatebin.js b/js/privatebin.js index 8d784cb5..118106c2 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -24,17 +24,23 @@ // Immediately start random number generator collector. sjcl.random.startCollectors(); +// main application start, called when DOM is fully loaded +jQuery(document).ready(function() { + // run main controller + $.PrivateBin.Controller.init(); +}); + jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { 'use strict'; /** - * static helper methods + * static Helper methods * * @param {object} window * @param {object} document * @class */ - var helper = (function (window, document) { + var Helper = (function (window, document) { var me = {}; /** @@ -62,12 +68,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @private * @enum {string|null} */ - var scriptLocation = null; + var baseUri = null; /** * converts a duration (in seconds) into human friendly approximation * - * @name helper.secondsToHuman + * @name Helper.secondsToHuman * @function * @param {number} seconds * @return {Array} @@ -104,7 +110,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * 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 + * @name Helper.selectText * @function * @param {HTMLElement} element */ @@ -113,15 +119,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var range, selection; // MS - if (document.body.createTextRange) - { + if (document.body.createTextRange) { range = document.body.createTextRange(); range.moveToElementText(element); range.select(); - } - // all others - else if (window.getSelection) - { + } else if (window.getSelection){ selection = window.getSelection(); range = document.createRange(); range.selectNodeContents(element); @@ -133,15 +135,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set text of a jQuery element (required for IE), * - * @name helper.setElementText + * @name Helper.setElementText * @function * @param {jQuery} $element - a jQuery element * @param {string} text - the text to enter */ me.setElementText = function($element, text) { - // @TODO: Can we drop IE 10 support? This function looks crazy and checking oldienotice slows everything down… - // I cannot really say, whether this IE10 method is XSS-safe… // 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
'); @@ -163,21 +163,21 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM= * * - * @name helper.urls2links + * @name Helper.urls2links * @function * @param {Object} element - a jQuery DOM element */ - me.urls2links = function(element) + me.urls2links = function($element) { var markup = '$1'; - element.html( - element.html().replace( + $element.html( + $element.html().replace( /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig, markup ) ); - element.html( - element.html().replace( + $element.html( + $element.html().replace( /((magnet):[\w?=&.\/-;#@~%+-]+)/ig, markup ) @@ -188,7 +188,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * 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 + * @name Helper.sprintf * @function * @param {string} format * @param {...*} args - one or multiple parameters injected into format string @@ -196,11 +196,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.sprintf = function() { - var args = arguments; - if (typeof arguments[0] === 'object') - { - args = arguments[0]; - } + var args = Array.prototype.slice.call(arguments); var format = args[0], i = 1; return format.replace(/%((%)|s|d)/g, function (m) { @@ -232,7 +228,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * get value of cookie, if it was set, empty string otherwise * * @see {@link http://www.w3schools.com/js/js_cookies.asp} - * @name helper.getCookie + * @name Helper.getCookie * @function * @param {string} cname * @return {string} @@ -255,75 +251,38 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * get the current script location (without search or hash part of the URL), + * get the current location (without search or hash part of the URL), * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/ * - * @name helper.scriptLocation + * @name Helper.baseUri * @function - * @return {string} current script location + * @return {string} */ - me.scriptLocation = function() + me.baseUri = function() { // check for cached version - if (scriptLocation !== null) { - return scriptLocation; + if (baseUri !== null) { + return baseUri; } - scriptLocation = window.location.href.substring( - 0, - window.location.href.length - window.location.search.length - window.location.hash.length - ); + // get official base uri string, from base tag in head of HTML + baseUri = document.baseURI; - var hashIndex = scriptLocation.indexOf('?'); - - if (hashIndex !== -1) - { - scriptLocation = scriptLocation.substring(0, hashIndex); + // if base uri contains query string (when no base tag is present), + // it is unwanted + if (baseUri.indexOf('?')) { + // so we built our own baseuri + baseUri = window.location.origin + window.location.pathname; } - 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; + return baseUri; }; /** * 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 + * @name Helper.htmlEntities * @function * @param {string} str * @return {string} escaped HTML @@ -339,15 +298,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(window, document); /** - * internationalization methods + * internationalization module * * @param {object} window * @param {object} document * @class */ - var i18n = (function (window, document) { + var I18n = (function (window, document) { var me = {}; + /** + * const for string of loaded language + * + * @private + * @prop {string} + * @readonly + */ + var languageLoadedEvent = 'languageLoaded'; + /** * supported languages, minus the built in 'en' * @@ -361,9 +329,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * built in language * * @private - * @prop {string} + * @prop {string|null} */ - var language = 'en'; + var language = null; /** * translation cache @@ -374,83 +342,125 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var translations = {}; /** - * translate a string, alias for i18n.translate() + * translate a string, alias for I18n.translate() * - * @name i18n._ + * for a full description see me.translate + * + * @name I18n._ * @function + * @param {jQuery} $element - optional * @param {string} messageId * @param {...*} args - one or multiple parameters injected into placeholders * @return {string} */ me._ = function() { - return me.translate(arguments); + return me.translate.apply(this, arguments); }; /** * translate a string * - * @name i18n.translate + * Optionally pass a jQuery element as the first parameter, to automatically + * let the text of this element be replaced. In case the (asynchronously + * loaded) language is not downloadet yet, this will make sure the string + * is replaced when it is actually loaded. + * So for easy translations passing the jQuery object to apply it to is + * more save, especially when they are loaded in the beginning. + * + * @name I18n.translate * @function + * @param {jQuery} $element - optional * @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]; + // convert parameters to array + var args = Array.prototype.slice.call(arguments), + messageId, + $element = null; + + // parse arguments + if (args[0] instanceof jQuery) { + // optional jQuery element as first parameter + $element = args[0]; + args.shift(); } + + // extract messageId from arguments var usesPlurals = $.isArray(args[0]); - if (usesPlurals) - { + if (usesPlurals) { // use the first plural form as messageId, otherwise the singular messageId = (args[0].length > 1 ? args[0][1] : args[0][0]); - } - else - { + } else { messageId = args[0]; } - if (messageId.length === 0) - { + + if (messageId.length === 0) { return messageId; } - if (!translations.hasOwnProperty(messageId)) - { - if (language !== 'en') - { - console.error( - 'Missing ' + language + ' translation for: ' + messageId - ); + + // if no translation string cannot be found (in translations object) + if (!translations.hasOwnProperty(messageId)) { + // if language is still loading and we have an elemt assigned + if (language === null && $element !== null) { + // handle the error by attaching the language loaded event + var orgArguments = arguments; + $(document).on(languageLoadedEvent, function () { + // re-execute this function + me.translate.apply(this, orgArguments); + // log to show that the previous error could be mitigated + console.log('Fixed missing translation of \'' + messageId + '\' with now loaded language ' + language); + }); + + // and fall back to English for now until the real language + // file is loaded } + + // for all other langauges than English for which thsi behaviour + // is expected as it is built-in, log error + if (language !== 'en') { + console.error('Missing translation for: \'' + messageId + '\' in language ' + language); + // fallback to English + } + + // save English translation (should be the same on both sides) translations[messageId] = args[0]; } - if (usesPlurals && $.isArray(translations[messageId])) - { + + // lookup plural translation + if (usesPlurals && $.isArray(translations[messageId])) { var n = parseInt(args[1] || 1, 10), key = me.getPluralForm(n), maxKey = translations[messageId].length - 1; - if (key > maxKey) - { + if (key > maxKey) { key = maxKey; } args[0] = translations[messageId][key]; args[1] = n; - } - else - { + } else { + // lookup singular translation args[0] = translations[messageId]; } - return helper.sprintf(args); + + // format string + var output = Helper.sprintf.apply(this, args); + + // if $element is given, apply text to element + if ($element !== null) { + $element.text(output); + } + + return output; }; /** * 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 + * @name I18n.getPluralForm * @function * @param {number} n * @return {number} array key @@ -475,37 +485,46 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * load translations into cache, then trigger controller initialization + * load translations into cache * - * @name i18n.loadTranslations + * @name I18n.loadTranslations * @function */ me.loadTranslations = function() { - var newLanguage = helper.getCookie('lang'); + var newLanguage = Helper.getCookie('lang'); // auto-select language based on browser settings - if (newLanguage.length === 0) - { + if (newLanguage.length === 0) { newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2); } - // if language is already used (e.g, default 'en'), skip update + // if language is already used skip update if (newLanguage === language) { return; } + // if language is built-in (English) skip update + if (newLanguage === 'en') { + language = 'en'; + 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); + language = 'en'; + return; } - // load strongs from JSON + // load strings from JSON $.getJSON('i18n/' + newLanguage + '.json', function(data) { language = newLanguage; translations = data; + $(document).triggerHandler(languageLoadedEvent); }).fail(function (data, textStatus, errorMsg) { console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg); + language = 'en'; }); }; @@ -517,7 +536,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @class */ - var cryptTool = (function () { + var CryptTool = (function () { var me = {}; /** @@ -551,7 +570,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * compress, then encrypt message with given key and password * - * @name cryptTool.cipher + * @name CryptTool.cipher * @function * @param {string} key * @param {string} password @@ -577,7 +596,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * decrypt message with key, then decompress * - * @name cryptTool.decipher + * @name CryptTool.decipher * @function * @param {string} key * @param {string} password @@ -610,7 +629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * checks whether the crypt tool is ready. * - * @name cryptTool.isReady + * @name CryptTool.isReady * @function * @return {bool} */ @@ -622,9 +641,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * checks whether the crypt tool is ready. * - * @name cryptTool.isReady + * @name CryptTool.isReady * @function - * @param {function} - the function to add + * @param {function} func */ me.addEntropySeedListener = function(func) { @@ -634,9 +653,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns a random symmetric key * - * @name cryptTool.getSymmetricKey + * @name CryptTool.getSymmetricKey * @function - * @return {string} + * @return {string} func */ me.getSymmetricKey = function(func) { @@ -646,16 +665,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * initialize crypt tool * - * @name cryptTool.init + * @name CryptTool.init * @function */ me.init = function() { // will fail earlier as sjcl is already passed as a parameter // if (typeof sjcl !== 'object') { - // alert.showError( - // i18n._('The library %s is not available.', 'sjcl') + - // i18n._('Messages cannot be decrypted or encrypted.') + // Alert.showError( + // I18n._('The library %s is not available.', 'sjcl') + + // I18n._('Messages cannot be decrypted or encrypted.') // ); // } }; @@ -664,45 +683,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(); /** - * Data source (aka MVC) + * (modal) Data source (aka MVC) * * @param {object} window * @param {object} document * @class */ - var modal = (function (window, document) { + var Modal = (function (window, document) { var me = {}; var $cipherData; - /** - * check if cipher data was supplied - * - * @name modal.getCipherData - * @function - * @return boolean - */ - me.hasCipherData = function() - { - return (me.getCipherData().length > 0); - }; - - /** - * returns the cipher data - * - * @name modal.getCipherData - * @function - * @return string - */ - me.getCipherData = function() - { - return $cipherData.text(); - }; + var id = null, symmetricKey = null; /** * returns the expiration set in the HTML * - * @name modal.getExpirationDefault + * @name Modal.getExpirationDefault * @function * @return string * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value) @@ -715,7 +712,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the format set in the HTML * - * @name modal.getFormatDefault + * @name Modal.getFormatDefault * @function * @return string * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value) @@ -725,12 +722,78 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return $('#pasteFormatter').val(); }; + /** + * check if cipher data was supplied + * + * @name Modal.getCipherData + * @function + * @return boolean + */ + me.hasCipherData = function() + { + return (me.getCipherData().length > 0); + }; + + /** + * returns the cipher data + * + * @name Modal.getCipherData + * @function + * @return string + */ + me.getCipherData = function() + { + return $cipherData.text(); + }; + + /** + * get the pastes unique identifier from the URL, + * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487 + * + * @name Modal.getPasteId + * @function + * @return {string} unique identifier + */ + me.getPasteId = function() + { + if (id === null) { + id = window.location.search.substring(1); + } + + return id; + }; + + /** + * return the deciphering key stored in anchor part of the URL + * + * @name Modal.getPasteKey + * @function + * @return {string} key + */ + me.getPasteKey = function() + { + if (symmetricKey === null) { + symmetricKey = window.location.hash.substring(1); + + // Some web 2.0 services and redirectors add data AFTER the anchor + // (such as &utm_source=...). We will strip any additional data. + var ampersandPos = symmetricKey.indexOf('&'); + if (ampersandPos > -1) + { + symmetricKey = symmetricKey.substring(0, ampersandPos); + } + + } + + return symmetricKey; + }; + /** * init navigation manager * * preloads jQuery elements * - * @name modal.init + * @name Modal.init * @function */ me.init = function() @@ -742,39 +805,29 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(window, document); /** - * User interface manager + * Helper functions for user interface + * + * everything directly UI-related, which fits nowhere else * * @param {object} window * @param {object} document * @class */ - var uiMan = (function (window, document) { + var UiHelper = (function (window, document) { var me = {}; - // jQuery pre-loaded objects - var $clearText, - $clonedFile, - $comments, - $discussion, - $image, - $prettyMessage, - $prettyPrint, - $editorTabs, - $remainingTime, - $replyStatus; - /** * handle history (pop) state changes * * currently this does only handle redirects to the home page. * - * @name controller.historyChange + * @private * @function * @param {Event} event */ - me.historyChange = function(event) + function historyChange(event) { - var currentLocation = helper.scriptLocation(); + var currentLocation = Helper.baseUri(); 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 @@ -787,62 +840,50 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * reload the page * - * This takes the user to the PrivateBin home page. + * This takes the user to the PrivateBin homepage. * - * @name controller.reloadPage + * @name UiHelper.reloadHome * @function - * @param {Event} event */ - me.reloadPage = function(event) + me.reloadHome = function() { - window.location.href = helper.scriptLocation(); - event.preventDefault(); + window.location.href = Helper.baseUri(); }; /** - * main UI manager + * initialize * - * @name controller.init + * @name UiHelper.init * @function */ me.init = function() { - // hide "no javascript" message - $('#noscript').hide(); + // update link to home page + $('.reloadlink').prop('href', Helper.baseUri()); - // bind events - $('.reloadlink').click(me.reloadPage); - - $(window).on('popstate', me.historyChange); + $(window).on('popstate', historyChange); }; return me; })(window, document); /** - * alert/notification manager + * Alert/error manager * * @param {object} window * @param {object} document * @class */ - var alert = (function (window, document) { + var Alert = (function (window, document) { var me = {}; - var $attachment, - $attachmentLink, - $errorMessage, - $clonedFile, - $fileWrap, - $status, - $pasteSuccess, - $shortenButton, - $pasteUrl; + var $errorMessage, + $status; /** * display a status message * - * @name controller.showStatus + * @name Alert.showStatus * @function * @param {string} message - text to display * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false @@ -853,102 +894,83 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $status.text(message); }; - /** - * 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) { - $replyalert.find('.spinner').removeClass('hidden') - } - $replyalert.text(message); - }; - /** * hides any status messages * - * @name controller.hideMessages + * @name Alert.hideMessages * @function */ me.hideMessages = function() { $status.html(' '); + $errorMessage.addClass('hidden'); }; /** * display an error message * - * @name alert.showError + * @name Alert.showError * @function * @param {string} message - text to display */ me.showError = function(message) { + console.error('Error shown: ' + message); + $errorMessage.removeClass('hidden'); - $errorMessage.find(':last').text(message); + $errorMessage.find(':last').text(' ' + message); }; /** - * display an error message + * init status manager * - * @name alert.showError - * @function - * @param {string} message - text to display - */ - me.showReplyError = function(message) - { - $replyalert.addClass('alert-danger'); - $replyalert.addClass($errorMessage.attr('class')); // @TODO ???? - - $replyalert.text(message); - }; - - /** - * removes the existing attachment + * preloads jQuery elements * - * @name alert.removeAttachment + * @name Alert.init * @function */ - me.removeAttachment = function() + me.init = 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'); + // hide "no javascript" message + $('#noscript').hide(); + + $errorMessage = $('#errormessage'); + $status = $('#status'); + + // 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? + + // display error message from php code + if ($errorMessage.text().length > 1) { + Alert.showError($errorMessage.text()); + } }; - /** - * checks if there is an attachment - * - * @name alert.hasAttachment - * @function - */ - me.hasAttachment = function() - { - return typeof $attachmentLink.attr('href') !== 'undefined' - }; + return me; + })(window, document); - /** - * return the attachment - * - * @name alert.getAttachment - * @function - * @returns {array} - */ - me.getAttachment = function() - { - return [ - $attachmentLink.attr('href'), - $attachmentLink.attr('download') - ]; - }; + /** + * handles paste status/result + * + * @param {object} window + * @param {object} document + * @class + */ + var PasteStatus = (function (window, document) { + var me = {}; + + var $pasteSuccess, + $shortenButton, + $pasteUrl, + $remainingTime; /** * forward to URL shortener @@ -963,45 +985,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { + encodeURIComponent($pasteUrl.attr('href')); } - /** - * reload the page - * - * This takes the user to the PrivateBin home page. - * - * @name controller.createPasteNotification - * @function - * @param {string} url - * @param {string} deleteUrl - */ - me.createPasteNotification = function(url, deleteUrl) - { - $('#pastelink').find(':first').html( - i18n._( - 'Your paste is %s (Hit [Ctrl]+[c] to copy)', - url, url - ) - ); - // save newly created element - $pasteUrl = $('#pasteurl'); - // and add click event - $pasteUrl.click(pasteLinkClick); - - // shorten button - $('#deletelink').html('' + i18n._('Delete data') + ''); - - // show result - $pasteSuccess.removeClass('hidden'); - // we pre-select the link so that the user only has to [Ctrl]+[c] the link - helper.selectText($pasteUrl[0]); - }; - /** * 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 + * @name Controller.pasteLinkClick * @function * @param {Event} event */ @@ -1014,127 +1004,196 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } } + /** + * creates a notification after a successfull paste upload + * + * @name PasteStatus.createPasteNotification + * @function + * @param {string} url + * @param {string} deleteUrl + */ + me.createPasteNotification = function(url, deleteUrl) + { + $('#pastelink').find(':first').html( + I18n._( + 'Your paste is %s (Hit [Ctrl]+[c] to copy)', + url, url + ) + ); + // save newly created element + $pasteUrl = $('#pasteurl'); + // and add click event + $pasteUrl.click(pasteLinkClick); + + // shorten button + $('#deletelink').html('' + I18n._('Delete data') + ''); + + // show result + $pasteSuccess.removeClass('hidden'); + // we pre-select the link so that the user only has to [Ctrl]+[c] the link + Helper.selectText($pasteUrl[0]); + }; + + /** + * shows the remaining time + * + * @function + * @param {object} pasteMetaData + */ + me.showRemainingTime = function(pasteMetaData) + { + if (pasteMetaData.burnafterreading) { + // display paste "for your eyes only" if it is deleted + + // actually remove paste, before we claim it is deleted + Controller.removePaste(Modal.getPasteId(), 'burnafterreading'); + + I18n._($remainingTime.find(':last'), "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again."); + $remainingTime.addClass('foryoureyesonly'); + + // discourage cloning (it cannot really be prevented) + TopNav.hideCloneButton(); + + } else if (pasteMetaData.expire_date) { + // display paste expiration + var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time), + expirationLabel = [ + 'This document will expire in %d ' + expiration[1] + '.', + 'This document will expire in %d ' + expiration[1] + 's.' + ]; + + I18n._($remainingTime.find(':last'), expirationLabel, expiration[0]); + $remainingTime.removeClass('foryoureyesonly') + } else { + // never expires + return; + } + + // in the end, display notification + $remainingTime.removeClass('hidden'); + }; + /** * init status manager * * preloads jQuery elements * - * @name alert.init + * @name Alert.init * @function */ me.init = function() { - // hide "no javascript" message - $('#noscript').hide(); - $shortenButton = $('#shortenbutton'); - $attachment = $('#attachment'); - $attachmentLink = $('#attachment a'); - $clonedFile = $('#clonedfile'); - $errorMessage = $('#errormessage'); - $fileWrap = $('#filewrap'); $pasteSuccess = $('#pasteSuccess'); - // $pasteUrl is saved in submitPasteUpload() if/after it is - // actually created - $status = $('#status'); - // @TODO $replyStatus … + // $pasteUrl is saved in me.createPasteNotification() after creation + $remainingTime = $('#remainingtime'); // bind elements $shortenButton.click(sendToShortener); - - // 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? - - // display error message from php code - if ($errorMessage.text().length > 1) { - me.showError($errorMessage.text()); - } }; return me; })(window, document); /** - * Passwort prompt + * password prompt * * @param {object} window * @param {object} document * @class */ - var prompt = (function (window, document) { + var Prompt = (function (window, document) { var me = {}; var $passwordModal, $passwordForm, $passwordDecrypt; + var password = '', + passwordCallback = null; + /** * ask the user for the password and set it * - * @name controller.requestPassword + * the callback set via setPasswordCallback is executed + * + * @name Prompt.requestPassword() * @function */ me.requestPassword = function() { - if ($passwordModal.length === 0) { - // old method for page template - var password = prompt(i18n._('Please enter the password for this paste:'), ''); - if (password === null) - { - // @TODO when does this happen? - throw 'password prompt canceled'; - } - if (password.length === 0) - { - // recursive… - me.requestPassword(); - } else { - $passwordInput.val(password); - me.displayMessages(); - } - } else { - // new bootstrap method - $passwordModal.modal(); - } + // show new bootstrap method + $passwordModal.modal({ + backdrop: 'static', + keyboard: false + }); }; /** - * decrypt using the password from the modal dialog + * get cached password or password from easy Prompt * - * @name controller.decryptPasswordModal + * If you do not get a password with this function, use + * requestPassword + * + * @name Prompt.getPassword * @function */ me.getPassword = function() { - if ($passwordDecrypt.val().length === 0) { - me.requestPassword(); + if (password.length !== 0) { + return password; } - return $passwordDecrypt.val(); - // $passwordInput.val($passwordDecrypt.val()); - // me.displayMessages(); + if ($passwordModal.length === 0) { + // old method for page template + var newPassword = Prompt(I18n._('Please enter the password for this paste:'), ''); + if (newPassword === null) { + throw 'password Prompt canceled'; + } + if (password.length === 0) { + // recursive… + me.getPassword(); + } else { + password = newPassword; + } + } + + return password; }; /** - * submit a password in the modal dialog + * setsthe callback called when password is entered * - * @name controller.submitPasswordModal + * @name Prompt.setPasswordCallback + * @function + * @param {functions} setPasswordCallback + */ + me.setPasswordCallback = function(callback) + { + passwordCallback = callback; + }; + + /** + * submit a password in the Modal dialog + * + * @private * @function * @param {Event} event */ - me.submitPasswordModal = function(event) + function submitPasswordModal(event) { - event.preventDefault(); + // get input + password = $passwordDecrypt.val(); + + // hide modal $passwordModal.modal('hide'); - }; + + if (passwordCallback !== null) { + passwordCallback(); + } + + event.preventDefault(); + } /** @@ -1142,7 +1201,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * preloads jQuery elements * - * @name controller.init + * @name Controller.init * @function */ me.init = function() @@ -1154,25 +1213,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // bind events // focus password input when it is shown - $passwordModal.on('shown.bs.modal', function () { + $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); + // handle Modal password submission + $passwordForm.submit(submitPasswordModal); }; return me; })(window, document); /** - * Manage paste/message input + * Manage paste/message input, and preview tab + * + * Note that the actual preview is handled by PasteViewer. * * @param {object} window * @param {object} document * @class */ - var editor = (function (window, document) { + var Editor = (function (window, document) { var me = {}; var $message, @@ -1185,7 +1245,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * support input of tab character * - * @name editor.supportTabs + * @name Editor.supportTabs * @function * @param {Event} event * @TODO doc what is @this here? @@ -1211,9 +1271,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * view the editor tab + * view the Editor tab * - * @name editor.viewEditor + * @name Editor.viewEditor * @function * @param {Event} event - optional */ @@ -1223,25 +1283,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $messageEdit.addClass('active'); $messagePreview.removeClass('active'); - pasteViewer.hide(); + PasteViewer.hide(); // reshow input $message.removeClass('hidden'); me.focusInput(); - // me.stateNewPaste(); // finish isPreview = false; - // if (typeof event === 'undefined') { - // event.preventDefault(); - // } // @TODO confirm this is not needed + + // prevent jumping of page to top + if (typeof event !== 'undefined') { + event.preventDefault(); + } } /** * view the preview tab * - * @name editor.viewPreview + * @name Editor.viewPreview * @function * @param {Event} event */ @@ -1256,20 +1317,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // show preview $('#errormessage').find(':last') - pasteViewer.setText($message.val()); - pasteViewer.trigger(); + PasteViewer.setText($message.val()); + PasteViewer.run(); // finish isPreview = true; - // if (typeof event === 'undefined') { - // event.preventDefault(); - // } // @TODO confirm this is not needed + + // prevent jumping of page to top + if (typeof event !== 'undefined') { + event.preventDefault(); + } } /** * get the state of the preview * - * @name editor.isPreview + * @name Editor.isPreview * @function */ me.isPreview = function() @@ -1278,9 +1341,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * reset the editor view + * reset the Editor view * - * @name editor.resetInput + * @name Editor.resetInput * @function */ me.resetInput = function() @@ -1295,9 +1358,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * shows the editor + * shows the Editor * - * @name editor.show + * @name Editor.show * @function */ me.show = function() @@ -1307,9 +1370,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * hides the editor + * hides the Editor * - * @name editor.reset + * @name Editor.reset * @function */ me.hide = function() @@ -1321,7 +1384,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * focuses the message input * - * @name editor.focusInput + * @name Editor.focusInput * @function */ me.focusInput = function() @@ -1332,7 +1395,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the current text * - * @name editor.getText + * @name Editor.getText * @function * @return {string} */ @@ -1346,7 +1409,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * preloads jQuery elements * - * @name editor.init + * @name Editor.init * @function */ me.init = function() @@ -1367,23 +1430,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(window, document); /** - * Parse and show paste. + * (view) Parse and show paste. * * @param {object} window * @param {object} document * @class */ - var pasteViewer = (function (window, document) { + var PasteViewer = (function (window, document) { var me = {}; - var $clearText, - $comments, - $discussion, - $image, + var $clonedFile, + $plainText, $placeholder, $prettyMessage, - $prettyPrint, - $remainingTime; + $prettyPrint; var text, format = 'plaintext', @@ -1393,7 +1453,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * apply the set format on paste and displays it * - * @name pasteViewer.parsePaste * @private * @function */ @@ -1405,8 +1464,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } // set text - helper.setElementText($clearText, text); - helper.setElementText($prettyPrint, text); + Helper.setElementText($plainText, text); + Helper.setElementText($prettyPrint, text); switch (format) { case 'markdown': @@ -1415,11 +1474,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { tables: true, tablesHeaderId: true }); - $clearText.html( + $plainText.html( converter.makeHtml(text) ); // add table classes from bootstrap css - $clearText.find('table').addClass('table-condensed table-bordered'); + $plainText.find('table').addClass('table-condensed table-bordered'); break; case 'syntaxhighlighting': // @TODO is this really needed or is "one" enough? @@ -1430,14 +1489,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $prettyPrint.html( prettyPrintOne( - helper.htmlEntities(text), null, true + Helper.htmlEntities(text), null, true ) ); // fall through, as the rest is the same default: // = 'plaintext' // convert URLs to clickable links - helper.urls2links($clearText); - helper.urls2links($prettyPrint); + Helper.urls2links($plainText); + Helper.urls2links($prettyPrint); $prettyPrint.css('white-space', 'pre-wrap'); $prettyPrint.css('word-break', 'normal'); @@ -1448,7 +1507,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * displays the paste * - * @name pasteViewer.show * @private * @function */ @@ -1464,234 +1522,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { switch (format) { case 'markdown': - $clearText.removeClass('hidden'); + $plainText.removeClass('hidden'); $prettyMessage.addClass('hidden'); break; default: - $clearText.addClass('hidden'); + $plainText.addClass('hidden'); $prettyMessage.removeClass('hidden'); break; } } - /** - * show decrypted text in the display area, including discussion (if open) - * - * @name pasteViewer.displayPaste - * @function - * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) - */ - me.displayPaste = function(paste) - { - paste = paste || $.parseJSON(modal.getCipherData()); - var key = helper.pageKey(), - password = $passwordInput.val(); - if (!$prettyPrint.hasClass('prettyprinted')) { - // Try to decrypt the paste. - try - { - if (paste.attachment) - { - var attachment = cryptTool.decipher(key, password, paste.attachment); - if (attachment.length === 0) - { - if (password.length === 0) - { - me.requestPassword(); - return; - } - attachment = cryptTool.decipher(key, password, paste.attachment); - } - if (attachment.length === 0) - { - throw 'failed to decipher attachment'; - } - - if (paste.attachmentname) - { - var attachmentname = cryptTool.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 = cryptTool.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) - { - pasteViewer.setFormat(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.' - ]; - $remainingTime.find(':last').text(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.')); - }); - $remainingTime.find(':last').text(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 = cryptTool.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 = cryptTool.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 pasteViewer.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 = $('#replytemplate'); - $reply.find('button').click( - {parentid: commentid}, - me.sendComment - ); - source.after($reply); - $replyStatus = $('#replystatus'); // when ID --> put into HTML - $('#replymessage').focus(); - }; - /** * sets the format in which the text is shown * - * @name pasteViewer.setFormat + * @name PasteViewer.setFormat * @function * @param {string} the the new format */ @@ -1706,7 +1550,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the current format * - * @name pasteViewer.setFormat + * @name PasteViewer.setFormat * @function * @return {string} */ @@ -1715,12 +1559,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return format; }; + /** + * returns whether the current view is pretty printed + * + * @name PasteViewer.isPrettyPrinted + * @function + * @return {bool} + */ + me.isPrettyPrinted = function() + { + return $prettyPrint.hasClass('prettyprinted'); + }; + /** * sets the text to show * - * @name editor.init + * @name Editor.init * @function - * @param {string} the text to show + * @param {string} newText the text to show */ me.setText = function(newText) { @@ -1733,10 +1589,10 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * show/update the parsed text (preview) * - * @name pasteViewer.trigger + * @name PasteViewer.run * @function */ - me.trigger = function() + me.run = function() { if (isChanged) { parsePaste(); @@ -1752,16 +1608,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * hide parsed text (preview) * - * @name pasteViewer.hide + * @name PasteViewer.hide * @function */ me.hide = function() { if (!isDisplayed) { - console.warn('pasteViewer was called to hide the parsed view, but it is already hidden.'); + console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.'); } - $clearText.addClass('hidden'); + $plainText.addClass('hidden'); $prettyMessage.addClass('hidden'); $placeholder.addClass('hidden'); @@ -1773,36 +1629,253 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * preloads jQuery elements * - * @name editor.init + * @name Editor.init * @function */ me.init = function() { - $clearText = $('#cleartext'); - $comments = $('#comments'); - $discussion = $('#discussion'); - $image = $('#image'); + $plainText = $('#plaintext'); $placeholder = $('#placeholder'); $prettyMessage = $('#prettymessage'); $prettyPrint = $('#prettyprint'); - $remainingTime = $('#remainingtime'); // check requirements if (typeof prettyPrintOne !== 'function') { - alert.showError( - i18n._('The library %s is not available.', 'pretty print') + - i18n._('This may cause display errors.') + Alert.showError( + I18n._('The library %s is not available.', 'pretty print') + + I18n._('This may cause display errors.') ); } if (typeof showdown !== 'object') { - alert.showError( - i18n._('The library %s is not available.', 'showdown') + - i18n._('This may cause display errors.') + Alert.showError( + I18n._('The library %s is not available.', 'showdown') + + I18n._('This may cause display errors.') ); } // get default option from template/HTML or fall back to set value - format = modal.getFormatDefault() || format; + format = Modal.getFormatDefault() || format; + }; + + return me; + })(window, document); + + /** + * (view) Show attachment and preview if possible + * + * @param {object} window + * @param {object} document + * @class + */ + var AttachmentViewer = (function (window, document) { + var me = {}; + + var $attachment, + $attachmentLink, + $clonedFile, + $attachmentPreview, + $fileWrap; + + var attachmentChanged = false, + attachmentHasPreview = false; + + /** + * sets the attachment but does not yet show it + * + * @name AttachmentViewer.setAttachment + * @function + * @param {string} attachmentData - base64-encoded data of file + * @param {string} fileName - optional, file name + */ + me.setAttachment = function(attachmentData, fileName) + { + var imagePrefix = 'data:image/'; + + $attachmentLink.attr('href', attachmentData); + if (typeof fileName !== 'undefined') { + $attachmentLink.attr('download', fileName); + } + + // if the attachment is an image, display it + if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) { + $attachmentPreview.html( + $(document.createElement('img')) + .attr('src', attachmentData) + .attr('class', 'img-thumbnail') + ); + attachmentHasPreview = true; + } + + attachmentChanged = true; + }; + + /** + * displays the attachment + * + * @name AttachmentViewer.showAttachment + * @function + */ + me.showAttachment = function() + { + $attachment.removeClass('hidden'); + + if (attachmentHasPreview) { + $attachmentPreview.removeClass('hidden'); + } + } + + /** + * removes the existing attachment + * + * @name AttachmentViewer.removeAttachment + * @function + */ + me.removeAttachment = function() + { + // (new) + $attachment.addClass('hidden'); + $attachmentPreview.addClass('hidden'); + + $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'); + + // reset internal variables + }; + + /** + * checks if there is an attachment + * + * @name AttachmentViewer.hasAttachment + * @function + */ + me.hasAttachment = function() + { + return typeof $attachmentLink.attr('href') !== 'undefined' + }; + + /** + * return the attachment + * + * @name AttachmentViewer.getAttachment + * @function + * @returns {array} + */ + me.getAttachment = function() + { + return [ + $attachmentLink.attr('href'), + $attachmentLink.attr('download') + ]; + }; + + /** + * initiate + * + * preloads jQuery elements + * + * @name AttachmentViewer.init + * @function + */ + me.init = function() + { + $attachmentPreview = $('#attachmentPreview'); + $attachment = $('#attachment'); + $attachmentLink = $('#attachment a'); + $clonedFile = $('#clonedfile'); + $fileWrap = $('#filewrap'); + }; + + return me; + })(window, document); + + /** + * (view) Shows discussion thread and handles replies + * + * @param {object} window + * @param {object} document + * @class + */ + var DiscussionViewer = (function (window, document) { + var me = {}; + + var $comments, + $discussion; + + /** + * 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) { + $replyalert.find('.spinner').removeClass('hidden') + } + $replyalert.text(message); + }; + + /** + * display an error message + * + * @name Alert.showError + * @function + * @param {string} message - text to display + */ + me.showReplyError = function(message) + { + $replyalert.addClass('Alert-danger'); + $replyalert.addClass($errorMessage.attr('class')); // @TODO ???? + + $replyalert.text(message); + }; + + /** + * open the comment entry when clicking the "Reply" button of a comment + * + * @name PasteViewer.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 = $('#replytemplate'); + $reply.find('button').click( + {parentid: commentid}, + me.sendComment + ); + source.after($reply); + $replyStatus = $('#replystatus'); // when ID --> put into HTML + $('#replymessage').focus(); + }; + + /** + * initiate + * + * preloads jQuery elements + * + * @name AttachmentViewer.init + * @function + */ + me.init = function() + { + $comments = $('#comments'); + $discussion = $('#discussion'); + // $replyStatus in openReply() }; return me; @@ -1813,10 +1886,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @param {object} window * @param {object} document - * @name state * @class */ - var topNav = (function (window, document) { + var TopNav = (function (window, document) { var me = {}; var createButtonsDisplayed = false; @@ -1843,7 +1915,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set the expiration on bootstrap templates in dropdown * - * @name topNav.updateExpiration + * @name TopNav.updateExpiration * @function * @param {Event} event */ @@ -1862,7 +1934,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set the format on bootstrap templates in dropdown * - * @name topNav.updateFormat + * @name TopNav.updateFormat * @function * @param {Event} event */ @@ -1874,11 +1946,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // update dropdown display and save new format var newFormat = $target.data('format'); $('#pasteFormatterDisplay').text($target.text()); - pasteViewer.setFormat(newFormat); + PasteViewer.setFormat(newFormat); // update preview - if (editor.isPreview()) { - pasteViewer.trigger(); + if (Editor.isPreview()) { + PasteViewer.run(); } event.preventDefault(); @@ -1887,7 +1959,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * when "burn after reading" is checked, disable discussion * - * @name topNav.changeBurnAfterReading + * @name TopNav.changeBurnAfterReading * @function */ function changeBurnAfterReading() @@ -1906,7 +1978,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * when discussion is checked, disable "burn after reading" * - * @name topNav.changeOpenDiscussion + * @name TopNav.changeOpenDiscussion * @function */ function changeOpenDiscussion() @@ -1925,44 +1997,44 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * return raw text * - * @name topNav.rawText + * @name TopNav.rawText * @function * @param {Event} event */ function rawText(event) { - var paste = pasteViewer.getFormat() === 'markdown' ? - $prettyPrint.text() : $clearText.text(); + var paste = PasteViewer.getFormat() === 'markdown' ? + $prettyPrint.text() : $plainText.text(); history.pushState( - null, document.title, helper.scriptLocation() + '?' + - helper.pasteId() + '#' + helper.pageKey() + null, document.title, Helper.baseUri() + '?' + + Modal.getPasteId() + '#' + Modal.getPasteKey() ); // 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.write('
' + Helper.htmlEntities(paste) + '
'); newDoc.close(); event.preventDefault(); } /** - * set the language in a cookie and reload the page + * saves the language in a cookie and reloads the page * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function * @param {Event} event */ function setLanguage(event) { document.cookie = 'lang=' + $(event.target).data('lang'); - me.reloadPage(event); + UiHelper.reloadHome(); } /** * Shows all elements belonging to viwing an existing pastes * - * @name topNav.hideAllElem + * @name TopNav.hideAllElem * @function */ me.showViewButtons = function() @@ -1981,7 +2053,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Hides all elements belonging to existing pastes * - * @name topNav.hideAllElem + * @name TopNav.hideAllElem * @function */ me.hideViewButtons = function() @@ -1991,6 +2063,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } + $newButton.removeClass('hidden'); $cloneButton.addClass('hidden'); $rawTextButton.addClass('hidden'); @@ -2000,7 +2073,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * shows all elements needed when creating a new paste * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.showCreateButtons = function() @@ -2026,7 +2099,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * shows all elements needed when creating a new paste * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.hideCreateButtons = function() @@ -2036,12 +2109,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } + $newButton.addClass('hidden'); $sendButton.addClass('hidden'); $expiration.addClass('hidden'); $formatter.addClass('hidden'); $burnAfterReadingOption.addClass('hidden'); $openDiscussionOption.addClass('hidden'); - $newButton.addClass('hidden'); $password.addClass('hidden'); $attach.addClass('hidden'); // $clonedFile.addClass('hidden'); // @TODO @@ -2052,18 +2125,40 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * only shows the "new paste" button * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.showNewPasteButton = function() { - $newButton.addClass('hidden'); + $newButton.removeClass('hidden'); + }; + + /** + * only hides the clone button + * + * @name TopNav.hideCloneButton + * @function + */ + me.hideCloneButton = function() + { + $cloneButton.addClass('hidden'); + }; + + /** + * only hides the raw text button + * + * @name TopNav.hideRawButton + * @function + */ + me.hideRawButton = function() + { + $rawTextButton.addClass('hidden'); }; /** * shows a loading message, optionally with a percentage * - * @name topNav.showLoading + * @name TopNav.showLoading * @function * @param {string} message optional, default: 'Loading…' * @param {int} percentage optional, default: null @@ -2072,10 +2167,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { // default message text if (typeof message === 'undefined') { - message = i18n._('Loading…'); + message = I18n._('Loading…'); } - console.log($loadingIndicator); // currently percentage parameter is ignored if (message !== null) { $loadingIndicator.find(':last').text(message); @@ -2086,7 +2180,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * hides the loading message * - * @name topNav.hideLoading + * @name TopNav.hideLoading * @function */ me.hideLoading = function() @@ -2097,7 +2191,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * collapses the navigation bar if nedded * - * @name topNav.collapseBar + * @name TopNav.collapseBar * @function */ me.collapseBar = function() @@ -2114,7 +2208,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the currently set expiration time * - * @name topNav.getExpiration + * @name TopNav.getExpiration * @function * @return {int} */ @@ -2126,7 +2220,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the currently selected file(s) * - * @name topNav.getFileList + * @name TopNav.getFileList * @function * @return {FileList|null} */ @@ -2149,7 +2243,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the state of the burn after reading checkbox * - * @name topNav.getExpiration + * @name TopNav.getExpiration * @function * @return {bool} */ @@ -2161,7 +2255,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the state of the discussion checkbox * - * @name topNav.getOpenDiscussion + * @name TopNav.getOpenDiscussion * @function * @return {bool} */ @@ -2173,7 +2267,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the entered password * - * @name topNav.getPassword + * @name TopNav.getPassword * @function * @return {string} */ @@ -2187,7 +2281,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * preloads jQuery elements * - * @name topNav.init + * @name TopNav.init * @function */ me.init = function() @@ -2216,9 +2310,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // bind events $burnAfterReading.change(changeBurnAfterReading); $openDiscussionOption.change(changeOpenDiscussion); - $newButton.click(controller.newPaste); - $sendButton.click(controller.submitPaste); - $cloneButton.click(controller.clonePaste); + $newButton.click(Controller.newPaste); + $sendButton.click(PasteEncrypter.submitPaste); + $cloneButton.click(Controller.clonePaste); $rawTextButton.click(rawText); $fileRemoveButton.click(me.removeAttachment); @@ -2231,7 +2325,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { changeOpenDiscussion(); // get default value from template or fall back to set value - pasteExpiration = modal.getExpirationDefault() || pasteExpiration; + pasteExpiration = Modal.getExpirationDefault() || pasteExpiration; }; return me; @@ -2240,21 +2334,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Responsible for AJAX requests, transparently handles encryption… * - * @name state * @class */ - var uploader = (function () { + var Uploader = (function () { var me = {}; var successFunc = null, - failureFunc = null; - - var url = helper.scriptLocation(), - data = {}, + failureFunc = null, + url, + data, randomKey, password; - // public variable ('constant') to prevent magic numbers + /** + * public variable ('constant') for errors to prevent magic numbers + * + * @readonly + * @enum {Object} + */ me.error = { okay: 0, custom: 1, @@ -2292,7 +2389,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * called after a upload failure * - * @name uploader.submitPasteUpload + * @name Uploader.submitPasteUpload * @function * @param {int} status - internal code * @param {int} data - original error code @@ -2307,12 +2404,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * actually uploads the data * - * @name uploader.submitPasteUpload + * @name Uploader.run * @function */ - me.trigger = function() + me.run = function() { - console.log(data); $.ajax({ type: 'POST', url: url, @@ -2338,7 +2434,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set success function * - * @name uploader.setSuccess + * @name Uploader.setSuccess + * @function + * @param {function} func + */ + me.setUrl = function(newUrl) + { + url = newUrl; + }; + + /** + * set success function + * + * @name Uploader.setSuccess * @function * @param {function} func */ @@ -2350,7 +2458,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set failure function * - * @name uploader.setSuccess + * @name Uploader.setSuccess * @function * @param {function} func */ @@ -2362,7 +2470,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * prepares a new upload * - * @name uploader.prepare + * @name Uploader.prepare * @function * @param {string} newPassword * @return {object} @@ -2372,33 +2480,35 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // set password password = newPassword; - // entropy should already be checked - // @TODO maybe move it here? + // entropy should already be checked! // generate a new random key - randomKey = cryptTool.getSymmetricKey(); + randomKey = CryptTool.getSymmetricKey(); // reset data + successFunc = null; + failureFunc = null; + url = Helper.baseUri() data = {}; }; /** * encrypts and sets the data * - * @name uploader.setData + * @name Uploader.setData * @function * @param {string} index * @param {mixed} element */ me.setData = function(index, element) { - data[index] = cryptTool.cipher(randomKey, password, element); + data[index] = CryptTool.cipher(randomKey, password, element); }; /** * set the additional metadata to send unencrypted * - * @name uploader.setUnencryptedData + * @name Uploader.setUnencryptedData * @function * @param {string} index * @param {mixed} element @@ -2411,7 +2521,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set the additional metadata to send unencrypted passed at once * - * @name uploader.setUnencryptedData + * @name Uploader.setUnencryptedData * @function * @param {object} newData */ @@ -2421,9 +2531,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * init uploader + * init Uploader * - * @name uploader.init + * @name Uploader.init * @function */ me.init = function() @@ -2435,52 +2545,124 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(); /** - * PrivateBin logic + * (controller) Responsible for encrypting paste and sending it to server. * - * @param {object} window - * @param {object} document - * @name controller + * @name state * @class */ - var controller = (function (window, document) { + var PasteEncrypter = (function () { var me = {}; + var requirementsChecked = false; + + /** + * checks whether there is a suitable amount of entrophy + * + * @private + * @function + * @param {function} retryCallback - the callback to execute to retry the upload + * @return {bool} + */ + function checkRequirements(retryCallback) { + // skip double requirement checks + if (requirementsChecked === true) { + return false; + } + + if (!CryptTool.isEntropyReady()) { + // display a message and wait + Alert.showStatus(I18n._('Please move your mouse for more entropy...')); + + CryptTool.addEntropySeedListener(retryCallback); + return false; + } + + requirementsChecked = true; + + return true; + } + /** * called after successful upload * + * @private * @function * @param {int} status * @param {int} data */ function showCreatedPaste(status, data) { - topNav.hideLoading(); - console.log(data); + TopNav.hideLoading(); - var url = helper.scriptLocation() + '?' + data.id + '#' + data.encryptionKey, - deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; + var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey, + deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - alert.hideMessages(); + Alert.hideMessages(); // show notification - alert.createPasteNotification(url, deleteUrl) + PasteStatus.createPasteNotification(url, deleteUrl) // show new URL in browser bar history.pushState({type: 'newpaste'}, document.title, url); - topNav.showViewButtons(); - editor.hide(); + TopNav.showViewButtons(); + TopNav.hideRawButton(); + Editor.hide(); // parse and show text // (preparation already done in me.submitPaste()) - pasteViewer.trigger(); + PasteViewer.run(); + } + + /** + * adds attachments to the Uploader + * + * @private + * @function + * @param {File|null|undefined} file - optional, falls back to cloned attachment + * @param {function} callback - excuted when action is successful + */ + function encryptAttachments(file, callback) { + if (typeof file !== 'undefined' && file !== null) { + // check file reader requirements for upload + if (typeof FileReader === 'undefined') { + Alert.showError(I18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + // cancels process as it does not execute callback + return; + } + + var reader = new FileReader(); + + // closure to capture the file information + reader.onload = function(event) { + Uploader.setData('attachment', event.target.result); + Uploader.setData('attachmentname', file.name); + + // run callback + callback(); + }; + + // actually read first file + reader.readAsDataURL(file); + } else if (AttachmentViewer.hasAttachment()) { + // fall back to cloned part + var attachment = AttachmentViewer.getAttachment(); + + Uploader.setData('attachment', attachment[0]); + Uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??! + callback(); + } else { + // if there are no attachments, this is of course still successful + callback(); + } } /** * send a reply in a discussion * - * @name controller.sendComment + * @name PasteEncrypter.sendComment * @function * @param {Event} event + * @TODO WIP */ me.sendComment = function(event) { @@ -2493,36 +2675,36 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - me.showStatus(i18n._('Sending comment...'), true); + me.showStatus(I18n._('Sending comment...'), true); var parentid = event.data.parentid, - key = helper.pageKey(), - cipherdata = cryptTool.cipher(key, $passwordInput.val(), replyMessage.val()), + key = Modal.getPasteKey(), + cipherdata = CryptTool.cipher(key, $passwordInput.val(), replyMessage.val()), ciphernickname = '', nick = $('#nickname').val(); if (nick.length > 0) { - ciphernickname = cryptTool.cipher(key, $passwordInput.val(), nick); + ciphernickname = CryptTool.cipher(key, $passwordInput.val(), nick); } var dataToSend = { data: cipherdata, parentid: parentid, - pasteid: helper.pasteId(), + pasteid: Modal.getPasteId(), nickname: ciphernickname }; $.ajax({ type: 'POST', - url: helper.scriptLocation(), + url: Helper.baseUri(), data: dataToSend, dataType: 'json', headers: ajaxHeaders, success: function(data) { if (data.status === 0) { - controller.showStatus(i18n._('Comment posted.')); + status.showStatus(I18n._('Comment posted.')); $.ajax({ type: 'GET', - url: helper.scriptLocation() + '?' + helper.pasteId(), + url: Helper.baseUri() + '?' + Modal.getPasteId(), dataType: 'json', headers: ajaxHeaders, success: function(data) { @@ -2532,157 +2714,396 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } else if (data.status === 1) { - alert.showError(i18n._('Could not refresh display: %s', data.message)); + Alert.showError(I18n._('Could not refresh display: %s', data.message)); } else { - alert.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); + Alert.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'))); + Alert.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)); + Alert.showError(I18n._('Could not post comment: %s', data.message)); } else { - controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); + Alert.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'))); + Alert.showError(I18n._('Could not post comment: %s', I18n._('server error or not responding'))); }); }; /** * sends a new paste to server * - * @name controller.submitPaste + * @name PasteEncrypter.submitPaste * @function */ me.submitPaste = function() { // UI loading state - topNav.hideCreateButtons(); - topNav.showLoading(i18n._('Sending paste...'), 0); - topNav.collapseBar(); + TopNav.hideCreateButtons(); + TopNav.showLoading(I18n._('Sending paste...'), 0); + TopNav.collapseBar(); // get data - var plainText = editor.getText(); + var plainText = Editor.getText(), + format = PasteViewer.getFormat(), + files = TopNav.getFileList(); // do not send if there is no data if (plainText.length === 0 && files === null) { // revert loading status… - topNav.hideLoading(); - topNav.showCreateButtons(); + TopNav.hideLoading(); + TopNav.showCreateButtons(); return; } - topNav.showLoading(i18n._('Sending paste...'), 10); + TopNav.showLoading(I18n._('Sending paste...'), 10); // check entropy - if (!cryptTool.isEntropyReady()) { - // display a message and wait - alert.showStatus(i18n._('Please move your mouse for more entropy...')); - - cryptTool.addEntropySeedListener(function() { - me.submitPaste(event); - }); + if (!checkRequirements(function () { + me.submitPaste(); + })) { + return; // to prevent multiple executions } - // prepare uploader - uploader.prepare(topNav.getPassword()); - - // encrypt cipher data - uploader.setData('data', plainText); - - // encrypt attachments - var files = topNav.getFileList(); - if (files !== null) { - var reader = new FileReader(); - - // closure to capture the file information - reader.onload = (function(file) { - return function(event) { - uploader.setData('attachment', event.target.result); - uploader.setData('attachmentname', file.name); - }; - })(files[0]); - - // actually read first file - reader.readAsDataURL(files[0]); - } else if (alert.hasAttachment()) { - var attachment = alert.getAttachment(); - - uploader.setData('attachment', attachment[0]); - uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??! - } + // prepare Uploader + Uploader.prepare(TopNav.getPassword()); // set success/fail functions - uploader.setSuccess(showCreatedPaste); - uploader.setFailure(function (status, data) { + Uploader.setSuccess(showCreatedPaste); + Uploader.setFailure(function (status, data) { // revert loading status… - topNav.hideLoading(); - topNav.showCreateButtons(); + TopNav.hideLoading(); + TopNav.showCreateButtons(); // show error message switch (status) { - case uploader.error['custom']: - alert.showError(i18n._('Could not create paste: %s', data.message)); + case Uploader.error['custom']: + Alert.showError(I18n._('Could not create paste: %s', data.message)); break; - case uploader.error['unknown']: - alert.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); + case Uploader.error['unknown']: + Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown status'))); break; - case uploader.error['serverError']: - alert.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); + case Uploader.error['serverError']: + Alert.showError(I18n._('Could not create paste: %s', I18n._('server error or not responding'))); break; default: - alert.showError(i18n._('Could not create paste: %s', i18n._('unknown error'))); + Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown error'))); break; } }); // fill it with unencrypted submitted options - var format = pasteViewer.getFormat(); - uploader.setUnencryptedBulkData({ - expire: topNav.getExpiration(), + Uploader.setUnencryptedBulkData({ + expire: TopNav.getExpiration(), formatter: format, - burnafterreading: topNav.getBurnAfterReading() ? 1 : 0, - opendiscussion: topNav.getOpenDiscussion() ? 1 : 0 + burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0, + opendiscussion: TopNav.getOpenDiscussion() ? 1 : 0 }); // prepare PasteViewer for later preview - pasteViewer.setText(plainText); - pasteViewer.setFormat(format); + PasteViewer.setText(plainText); + PasteViewer.setFormat(format); - // send data - uploader.trigger(); + // encrypt cipher data + Uploader.setData('data', plainText); + + // encrypt attachments + encryptAttachments( + files === null ? null : files[0], + function () { + // send data + Uploader.run(); + } + ); }; + /** + * initialize + * + * @name PasteEncrypter.init + * @function + */ + me.init = function() + { + // nothing yet + }; + + return me; + })(); + + /** + * (controller) Responsible for decrypting cipherdata and passing data to view. + * + * @name state + * @class + */ + var PasteDecrypter = (function () { + var me = {}; + + /** + * decrypt the actual paste text + * + * @private + * @function + * @param {object} paste - paste data in object form + * @param {string} key + * @param {string} password + * @return {bool} - whether action was successful + */ + function decryptPaste(paste, key, password) + { + // try decryption without password + var plaintext = CryptTool.decipher(key, password, paste.data); + + // if it fails, request password + if (plaintext.length === 0 && password.length === 0) { + // get password + password = Prompt.getPassword(); + + // if password is there, re-try + if (password.length !== 0) { + // recursive + // note: an infinite loop is prevented as the previous if + // clause checks whether a password is already set and ignores + // error with password being passed + return decryptPaste(paste, key, password); + } + + // trigger password request + Prompt.requestPassword(); + // the callback (via setPasswordCallback()) should have been set + // by parent function + return false; + } + + // if all tries failed, we can only throw an error + if (plaintext.length === 0) { + throw 'failed to decipher message'; + } + + // on success show paste + PasteViewer.setFormat(paste.meta.formatter); + PasteViewer.setText(plaintext); + // trigger to show the text (attachment loaded afterwards) + PasteViewer.run(); + + return true; + } + + /** + * decrypts any attachment + * + * @private + * @function + * @param {object} paste - paste data in object form + * @param {string} key + * @param {string} password + * @return {bool} - whether action was successful + */ + function decryptAttachment(paste, key, password) + { + // decrypt attachment + var attachment = CryptTool.decipher(key, password, paste.attachment); + if (attachment.length === 0) { + throw 'failed to decipher attachment'; + } + + // decrypt attachment name + var attachmentName; + if (paste.attachmentname) { + attachmentName = attachmentName = CryptTool.decipher(key, password, paste.attachmentname); + if (attachmentName.length === 0) { + // @TODO considering the buggy cloning (?, see other todo comment) this might affect previous pastes + throw 'failed to decipher attachment name'; + } + } + + AttachmentViewer.setAttachment(attachment, attachmentName); + AttachmentViewer.showAttachment(); + } + + /** + * show decrypted text in the display area, including discussion (if open) + * + * @name PasteDecrypter.run + * @function + * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) + */ + me.run = function(paste) + { + TopNav.showLoading('Decrypting paste…'); + + if (typeof paste === 'undefined') { + paste = $.parseJSON(Modal.getCipherData()); + } + + var key = Modal.getPasteKey(), + password = Prompt.getPassword(); + + if (PasteViewer.isPrettyPrinted()) { + console.error('Too pretty! (don\'t know why this check)'); //@TODO + return; + } + + // try to decrypt the paste + try { + Prompt.setPasswordCallback(function () { + me.run(paste); + }); + + // try to decrypt paste and if it fails (because the password is + // missing) return to let JS continue and wait for user + if (!decryptPaste(paste, key, password)) { + return; + } + + // decrypt attachments + if (paste.attachment) { + decryptAttachment(paste, key, password); + } + } catch(err) { + TopNav.hideLoading(); + + // log and show error + console.error(err); + Alert.showError(I18n._('Could not decrypt data (Wrong key?)')); // @TODO error is not translated + + // still go on to potentially show potentially partially decrypted data + } + + // shows the remaining time (until) deletion + PasteStatus.showRemainingTime(paste.meta); + + // if the discussion is opened on this paste, display it + // @TODO BELOW + 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 = CryptTool.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 = CryptTool.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: Modal.getPasteId()}, me.openReply); + $comments.append($divComment); + $discussion.removeClass('hidden'); + } + + TopNav.hideLoading(); + TopNav.showViewButtons(); + }; + + /** + * initialize + * + * @name PasteDecrypter.init + * @function + */ + me.init = function() + { + // nothing yet + }; + + return me; + })(); + + /** + * (controller) main PrivateBin logic + * + * @param {object} window + * @param {object} document + * @class + */ + var Controller = (function (window, document) { + var me = {}; + /** * creates a new paste * - * @name controller.newPaste + * @name Controller.newPaste * @function */ me.newPaste = function() { - topNav.hideViewButtons(); - topNav.showCreateButtons(); - editor.resetInput(); - editor.show(); - editor.focusInput(); + TopNav.hideViewButtons(); + TopNav.showCreateButtons(); + PasteViewer.hide(); + Editor.resetInput(); + Editor.show(); + Editor.focusInput(); + Alert.hideMessages(); }; /** * clone the current paste * - * @name controller.clonePaste + * @name Controller.clonePaste * @function * @param {Event} event */ @@ -2691,64 +3112,79 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.stateNewPaste(); // erase the id and the key in url - history.replaceState(null, document.title, helper.scriptLocation()); + history.replaceState(null, document.title, Helper.baseUri()); - alert.hideMessages(); + Alert.hideMessages(); if ($attachmentLink.attr('href')) { $clonedFile.removeClass('hidden'); $fileWrap.addClass('hidden'); } $message.val( - pasteViewer.getFormat() === 'markdown' ? - $prettyPrint.val() : $clearText.val() + PasteViewer.getFormat() === 'markdown' ? + $prettyPrint.val() : $plainText.val() ); - $('.navbar-toggle').click(); + TopNav.collapseBar(); + }; - event.preventDefault(); + /** + * removes a saved paste + * + * @name Controller.removePaste + * @function + * @param {string} pasteId + * @param {string} deleteToken + */ + me.removePaste = function(pasteId, deleteToken) { + // unfortunately many web servers don't support DELETE (and PUT) out of the box + // so we use a POST request + Uploader.prepare(); + Uploader.setUrl(Helper.baseUri() + '?' + pasteId); + Uploader.setUnencryptedData('deletetoken', deleteToken); + + Uploader.setFailure(function () { + Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); + }) + Uploader.run(); }; /** * application start * - * @name controller.init + * @name Controller.init * @function */ me.init = function() { // first load translations - i18n.loadTranslations(); + I18n.loadTranslations(); // initialize other modules/"classes" - alert.init(); - uploader.init(); - modal.init(); - cryptTool.init(); - uiMan.init(); - topNav.init(); - editor.init(); - pasteViewer.init(); - prompt.init(); + Alert.init(); + Uploader.init(); + Modal.init(); + CryptTool.init(); + UiHelper.init(); + TopNav.init(); + Editor.init(); + PasteStatus.init(); + PasteViewer.init(); + AttachmentViewer.init(); + DiscussionViewer.init(); + PasteEncrypter.init(); + PasteDecrypter.init(); + Prompt.init(); // display an existing paste - if (modal.hasCipherData()) { + if (Modal.hasCipherData()) { // missing decryption key in URL? - if (window.location.hash.length === 0) - { - alert.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?)')); + if (window.location.hash.length === 0) { + Alert.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 - // topNav.hideCreateButtons(); // they should not be visible in the first place - topNav.showViewButtons(); - me.displayMessages(); - return; - } - - // check requirements for upload - if (typeof FileReader === 'undefined') { - alert.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + PasteDecrypter.run(); return; } @@ -2759,21 +3195,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return me; })(window, document); - jQuery(document).ready(function() { - /** - * main application start, called when DOM is fully loaded and - * runs controller initalization - */ - $(controller.init); - }); - return { - helper: helper, - i18n: i18n, - cryptTool: cryptTool, - topNav: topNav, - alert: alert, - uploader: uploader, - controller: controller + Helper: Helper, + I18n: I18n, + CryptTool: CryptTool, + TopNav: TopNav, + Alert: Alert, + Uploader: Uploader, + Controller: Controller }; }(jQuery, sjcl, Base64, RawDeflate); diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index b9e3f606..75275b55 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -132,7 +132,7 @@ if ($isPage): -