* magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7 @@ -167,12 +297,12 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.sprintf = function() { - var args = Array.prototype.slice.call(arguments); - var format = args[0], + const args = Array.prototype.slice.call(arguments); + let format = args[0], i = 1; return format.replace(/%(s|d)/g, function (m) { // m is the matched format, e.g. %s, %d - var val = args[i]; + let val = args[i]; // A switch statement so that the formatter can be extended. switch (m) { @@ -200,10 +330,10 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @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]; + const name = cname + '=', + ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; ++i) { + let c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1); @@ -231,11 +361,55 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { return baseUri; } - // window.location.origin is a newer alternative, but requires FF 21 / Chrome 31 / Safari 7 / IE 11 - baseUri = window.location.protocol + '//' + window.location.host + window.location.pathname; + baseUri = window.location.origin + window.location.pathname; return baseUri; }; + + /** + * checks whether this is a bot we dislike + * + * @name Helper.isBadBot + * @function + * @return {bool} + */ + me.isBadBot = function() { + // check whether a bot user agent part can be found in the current + // user agent + for (let i = 0; i < BadBotUA.length; ++i) { + if (navigator.userAgent.indexOf(BadBotUA) >= 0) { + return true; + } + } + return false; + } + + /** + * wrap an object into a Paste, used for mocking in the unit tests + * + * @name Helper.PasteFactory + * @function + * @param {object} data + * @return {Paste} + */ + me.PasteFactory = function(data) + { + return new Paste(data); + }; + + /** + * wrap an object into a Comment, used for mocking in the unit tests + * + * @name Helper.CommentFactory + * @function + * @param {object} data + * @return {Comment} + */ + me.CommentFactory = function(data) + { + return new Comment(data); + }; + /** * resets state, used for unit testing * @@ -247,26 +421,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { baseUri = null; }; - /** - * checks whether this is a bot we dislike - * - * @name Helper.isBadBot - * @function - * @return {bool} - */ - me.isBadBot = function() { - // check whether a bot user agent part can be found in the current - // user agent - var arrayLength = BadBotUA.length; - for (var i = 0; i < arrayLength; i++) { - if (navigator.userAgent.indexOf(BadBotUA) >= 0) { - return true; - } - } - - return false; - } - return me; })(); @@ -276,8 +430,8 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name I18n * @class */ - var I18n = (function () { - var me = {}; + const I18n = (function () { + const me = {}; /** * const for string of loaded language @@ -287,7 +441,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @prop {string} * @readonly */ - var languageLoadedEvent = 'languageLoaded'; + const languageLoadedEvent = 'languageLoaded'; /** * supported languages, minus the built in 'en' @@ -297,7 +451,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @prop {string[]} * @readonly */ - var supportedLanguages = ['de', 'es', 'fr', 'it', 'hu', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh']; + const supportedLanguages = ['de', 'es', 'fr', 'it', 'hu', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh']; /** * built in language @@ -306,7 +460,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @private * @prop {string|null} */ - var language = null; + let language = null; /** * translation cache @@ -315,7 +469,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @private * @enum {Object} */ - var translations = {}; + let translations = {}; /** * translate a string, alias for I18n.translate @@ -352,7 +506,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { me.translate = function() { // convert parameters to array - var args = Array.prototype.slice.call(arguments), + let args = Array.prototype.slice.call(arguments), messageId, $element = null; @@ -364,7 +518,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } // extract messageId from arguments - var usesPlurals = $.isArray(args[0]); + let 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]; @@ -381,7 +535,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // 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; + let orgArguments = arguments; $(document).on(languageLoadedEvent, function () { // log to show that the previous error could be mitigated console.warn('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language); @@ -406,7 +560,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // lookup plural translation if (usesPlurals && $.isArray(translations[messageId])) { - var n = parseInt(args[1] || 1, 10), + let n = parseInt(args[1] || 1, 10), key = me.getPluralForm(n), maxKey = translations[messageId].length - 1; if (key > maxKey) { @@ -420,12 +574,12 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } // format string - var output = Helper.sprintf.apply(this, args); + let output = Helper.sprintf.apply(this, args); // if $element is given, apply text to element if ($element !== null) { // get last text node of element - var content = $element.contents(); + let content = $element.contents(); if (content.length > 1) { content[content.length - 1].nodeValue = ' ' + output; } else { @@ -472,7 +626,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.loadTranslations = function() { - var newLanguage = Helper.getCookie('lang'); + let newLanguage = Helper.getCookie('lang'); // auto-select language based on browser settings if (newLanguage.length === 0) { @@ -529,124 +683,444 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name CryptTool * @class */ - var CryptTool = (function () { - var me = {}; + const CryptTool = (function () { + const me = {}; /** - * compress a message (deflate compression), returns base64 encoded data + * base58 encoder & decoder * - * @name CryptTool.compress + * @private + */ + let base58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); + + /** + * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString + * + * Iterates over the bytes of the message, converting them all hexadecimal + * percent encoded representations, then URI decodes them all + * + * @name CryptTool.utf8To16 * @function * @private - * @param {string} message - * @return {string} base64 data + * @param {string} message UTF-8 string + * @return {string} UTF-16 string */ - function compress(message) + function utf8To16(message) { - return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) ); + return decodeURIComponent( + message.split('').map( + function(character) + { + return '%' + ('00' + character.charCodeAt(0).toString(16)).slice(-2); + } + ).join('') + ); } /** - * decompress a message compressed with cryptToolcompress() + * convert DOMString (UTF-16) to a UTF-8 string stored in a DOMString * - * @name CryptTool.decompress + * URI encodes the message, then finds the percent encoded characters + * and transforms these hexadecimal representation back into bytes + * + * @name CryptTool.utf16To8 * @function * @private - * @param {string} data - base64 data + * @param {string} message UTF-16 string + * @return {string} UTF-8 string + */ + function utf16To8(message) + { + return encodeURIComponent(message).replace( + /%([0-9A-F]{2})/g, + function (match, hexCharacter) + { + return String.fromCharCode('0x' + hexCharacter); + } + ); + } + + /** + * convert ArrayBuffer into a UTF-8 string + * + * Iterates over the bytes of the array, catenating them into a string + * + * @name CryptTool.arraybufferToString + * @function + * @private + * @param {ArrayBuffer} messageArray * @return {string} message */ - function decompress(data) + function arraybufferToString(messageArray) { - return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) ); + const array = new Uint8Array(messageArray); + let message = '', + i = 0; + while(i < array.length) { + message += String.fromCharCode(array[i++]); + } + return message; + } + + /** + * convert UTF-8 string into a Uint8Array + * + * Iterates over the bytes of the message, writing them to the array + * + * @name CryptTool.stringToArraybuffer + * @function + * @private + * @param {string} message UTF-8 string + * @return {Uint8Array} array + */ + function stringToArraybuffer(message) + { + const messageArray = new Uint8Array(message.length); + for (let i = 0; i < message.length; ++i) { + messageArray[i] = message.charCodeAt(i); + } + return messageArray; + } + + /** + * compress a string (deflate compression), returns buffer + * + * @name CryptTool.compress + * @async + * @function + * @private + * @param {string} message + * @param {string} mode + * @return {ArrayBuffer} data + */ + async function compress(message, mode) + { + message = stringToArraybuffer( + utf16To8(message) + ); + if (mode === 'zlib') { + return z.deflate(message).buffer; + } + return message; + } + + /** + * decompress potentially base64 encoded, deflate compressed buffer, returns string + * + * @name CryptTool.decompress + * @async + * @function + * @private + * @param {ArrayBuffer} data + * @param {string} mode + * @return {string} message + */ + async function decompress(data, mode) + { + if (mode === 'zlib' || mode === 'none') { + if (mode === 'zlib') { + data = z.inflate( + new Uint8Array(data) + ).buffer; + } + return utf8To16( + arraybufferToString(data) + ); + } + // detect presence of Base64.js, indicating legacy ZeroBin paste + if (typeof Base64 === 'undefined') { + return utf8To16( + RawDeflate.inflate( + utf8To16( + atob( + arraybufferToString(data) + ) + ) + ) + ); + } else { + return Base64.btou( + RawDeflate.inflate( + Base64.fromBase64( + arraybufferToString(data) + ) + ) + ); + } + } + + /** + * returns specified number of random bytes + * + * @name CryptTool.getRandomBytes + * @function + * @private + * @param {int} length number of random bytes to fetch + * @throws {string} + * @return {string} random bytes + */ + function getRandomBytes(length) + { + if ( + typeof window !== 'undefined' && + typeof Uint8Array !== 'undefined' && + String.fromCodePoint && + ( + typeof window.crypto !== 'undefined' || + typeof window.msCrypto !== 'undefined' + ) + ) { + // modern browser environment + let bytes = ''; + const byteArray = new Uint8Array(length), + crypto = window.crypto || window.msCrypto; + crypto.getRandomValues(byteArray); + for (let i = 0; i < length; ++i) { + bytes += String.fromCharCode(byteArray[i]); + } + return bytes; + } else { + // legacy browser or unsupported environment + throw 'No supported crypto API detected, you may read pastes and comments, but can\'t create pastes or add new comments.'; + } + } + + /** + * derive cryptographic key from key string and password + * + * @name CryptTool.deriveKey + * @async + * @function + * @private + * @param {string} key + * @param {string} password + * @param {array} spec cryptographic specification + * @return {CryptoKey} derived key + */ + async function deriveKey(key, password, spec) + { + let keyArray = stringToArraybuffer(key); + if (password.length > 0) { + // version 1 pastes did append the passwords SHA-256 hash in hex + if (spec[7] === 'rawdeflate') { + let passwordBuffer = await window.crypto.subtle.digest( + {name: 'SHA-256'}, + stringToArraybuffer( + utf16To8(password) + ) + ); + password = Array.prototype.map.call( + new Uint8Array(passwordBuffer), + x => ('00' + x.toString(16)).slice(-2) + ).join(''); + } + let passwordArray = stringToArraybuffer(password), + newKeyArray = new Uint8Array(keyArray.length + passwordArray.length); + newKeyArray.set(keyArray, 0); + newKeyArray.set(passwordArray, keyArray.length); + keyArray = newKeyArray; + } + + // import raw key + const importedKey = await window.crypto.subtle.importKey( + 'raw', // only 'raw' is allowed + keyArray, + {name: 'PBKDF2'}, // we use PBKDF2 for key derivation + false, // the key may not be exported + ['deriveKey'] // we may only use it for key derivation + ); + + // derive a stronger key for use with AES + return window.crypto.subtle.deriveKey( + { + name: 'PBKDF2', // we use PBKDF2 for key derivation + salt: stringToArraybuffer(spec[1]), // salt used in HMAC + iterations: spec[2], // amount of iterations to apply + hash: {name: 'SHA-256'} // can be "SHA-1", "SHA-256", "SHA-384" or "SHA-512" + }, + importedKey, + { + name: 'AES-' + spec[6].toUpperCase(), // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC") + length: spec[3] // can be 128, 192 or 256 + }, + false, // the key may not be exported + ['encrypt', 'decrypt'] // we may only use it for en- and decryption + ); + } + + /** + * gets crypto settings from specification and authenticated data + * + * @name CryptTool.cryptoSettings + * @function + * @private + * @param {string} adata authenticated data + * @param {array} spec cryptographic specification + * @return {object} crypto settings + */ + function cryptoSettings(adata, spec) + { + return { + name: 'AES-' + spec[6].toUpperCase(), // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC") + iv: stringToArraybuffer(spec[0]), // the initialization vector you used to encrypt + additionalData: stringToArraybuffer(adata), // the addtional data you used during encryption (if any) + tagLength: spec[4] // the length of the tag you used to encrypt (if any) + }; } /** * compress, then encrypt message with given key and password * * @name CryptTool.cipher + * @async * @function * @param {string} key * @param {string} password * @param {string} message - * @return {string} data - JSON with encrypted data + * @param {array} adata + * @return {array} encrypted message in base64 encoding & adata containing encryption spec */ - me.cipher = function(key, password, message) + me.cipher = async function(key, password, message, adata) { - // 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, compress(message), options); + // AES in Galois Counter Mode, keysize 256 bit, + // authentication tag 128 bit, 10000 iterations in key derivation + const spec = [ + getRandomBytes(16), // initialization vector + getRandomBytes(8), // salt + 100000, // iterations + 256, // key size + 128, // tag size + 'aes', // algorithm + 'gcm', // algorithm mode + 'zlib' // compression + ], encodedSpec = []; + for (let i = 0; i < spec.length; ++i) { + encodedSpec[i] = i < 2 ? btoa(spec[i]) : spec[i]; } - return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options); + if (adata.length === 0) { + // comment + adata = encodedSpec; + } else if (adata[0] === null) { + // paste + adata[0] = encodedSpec; + } + + // finally, encrypt message + return [ + btoa( + arraybufferToString( + await window.crypto.subtle.encrypt( + cryptoSettings(JSON.stringify(adata), spec), + await deriveKey(key, password, spec), + await compress(message, spec[7]) + ) + ) + ), + adata + ]; }; /** * decrypt message with key, then decompress * * @name CryptTool.decipher + * @async * @function * @param {string} key * @param {string} password - * @param {string} data - JSON with encrypted data + * @param {string|object} data encrypted message * @return {string} decrypted message, empty if decryption failed */ - me.decipher = function(key, password, data) + me.decipher = async function(key, password, data) { - if (data !== undefined) { - try { - return decompress(sjcl.decrypt(key, data)); - } catch(err) { - try { - return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data)); - } catch(e) { - return ''; - } - } + let adataString, encodedSpec, cipherMessage; + if (data instanceof Array) { + // version 2 + adataString = JSON.stringify(data[1]); + encodedSpec = (data[1][0] instanceof Array ? data[1][0] : data[1]); + cipherMessage = data[0]; + } else if (typeof data === 'string') { + // version 1 + let object = JSON.parse(data); + adataString = atob(object.adata); + encodedSpec = [ + object.iv, + object.salt, + object.iter, + object.ks, + object.ts, + object.cipher, + object.mode, + 'rawdeflate' + ]; + cipherMessage = object.ct; + } else { + throw 'unsupported message format'; + } + let spec = encodedSpec, plainText = ''; + spec[0] = atob(spec[0]); + spec[1] = atob(spec[1]); + try { + return await decompress( + await window.crypto.subtle.decrypt( + cryptoSettings(adataString, spec), + await deriveKey(key, password, spec), + stringToArraybuffer( + atob(cipherMessage) + ) + ), + encodedSpec[7] + ); + } catch(err) { + return ''; } - }; - - /** - * checks whether the crypt tool has collected enough entropy - * - * @name CryptTool.isEntropyReady - * @function - * @return {bool} - */ - me.isEntropyReady = function() - { - return sjcl.random.isReady(); - }; - - /** - * add a listener function, triggered when enough entropy is available - * - * @name CryptTool.addEntropySeedListener - * @function - * @param {function} func - */ - me.addEntropySeedListener = function(func) - { - sjcl.random.addEventListener('seeded', func); }; /** * returns a random symmetric key * + * generates 256 bit long keys (8 Bits * 32) for AES with 256 bit long blocks + * * @name CryptTool.getSymmetricKey * @function - * @return {string} func + * @throws {string} + * @return {string} raw bytes */ me.getSymmetricKey = function() { - var bs58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); - return bs58.encode(sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 10), 0)); + return getRandomBytes(32); }; + /** + * base58 encode a DOMString (UTF-16) + * + * @name CryptTool.base58encode + * @function + * @param {string} input + * @return {string} output + */ + me.base58encode = function(input) + { + return base58.encode( + stringToArraybuffer(input) + ); + } + + /** + * base58 decode a DOMString (UTF-16) + * + * @name CryptTool.base58decode + * @function + * @param {string} input + * @return {string} output + */ + me.base58decode = function(input) + { + return arraybufferToString( + base58.decode(input) + ); + } + return me; })(); @@ -656,14 +1130,14 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name Model * @class */ - var Model = (function () { - var me = {}; + const Model = (function () { + const me = {}; - var pasteData = null, + let id = null, + pasteData = null, + symmetricKey = null, $templates; - var id = null, symmetricKey = null; - /** * returns the expiration set in the HTML * @@ -712,25 +1186,25 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } // reload data - Uploader.prepare(); - Uploader.setUrl(Helper.baseUri() + '?' + me.getPasteId()); + ServerInteraction.prepare(); + ServerInteraction.setUrl(Helper.baseUri() + '?' + me.getPasteId()); - Uploader.setFailure(function (status, data) { + ServerInteraction.setFailure(function (status, data) { // revert loading status… Alert.hideLoading(); TopNav.showViewButtons(); // show error message - Alert.showError(Uploader.parseUploadError(status, data, 'get paste data')); + Alert.showError(ServerInteraction.parseUploadError(status, data, 'get paste data')); }); - Uploader.setSuccess(function (status, data) { - pasteData = data; + ServerInteraction.setSuccess(function (status, data) { + pasteData = new Paste(data); if (typeof callback === 'function') { - return callback(data); + return callback(pasteData); } }); - Uploader.run(); + ServerInteraction.run(); }; /** @@ -771,12 +1245,8 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // fallback below console.error('URL interface not properly supported, error:', e); } - } else { - console.warn('URL interface appears not to be supported in this browser.'); } - // fallback to simple RegEx - console.warn('fallback to simple RegEx search'); // Attention: This also returns the delete token inside of the ID, if it is specified id = (window.location.search.match(idRegExFind) || [''])[0]; @@ -788,7 +1258,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } /** - * Returns true, when the URL has a delete token and the current call was used for deleting a paste. + * returns true, when the URL has a delete token and the current call was used for deleting a paste. * * @name Model.hasDeleteToken * @function @@ -810,18 +1280,26 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { me.getPasteKey = function() { if (symmetricKey === null) { - symmetricKey = window.location.hash.substring(1); - - if (symmetricKey === '') { + let newKey = window.location.hash.substring(1); + if (newKey === '') { throw 'no encryption key given'; } // 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('&'); + let ampersandPos = newKey.indexOf('&'); if (ampersandPos > -1) { - symmetricKey = symmetricKey.substring(0, ampersandPos); + newKey = newKey.substring(0, ampersandPos); + } + + // version 2 uses base58, version 1 uses base64 without decoding + try { + // base58 encode strips NULL bytes at the beginning of the + // string, so we re-add them if necessary + symmetricKey = CryptTool.base58decode(newKey).padStart(32, '\u0000'); + } catch(e) { + symmetricKey = newKey; } } @@ -839,7 +1317,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { me.getTemplate = function(name) { // find template - var $element = $templates.find('#' + name + 'template').clone(true); + let $element = $templates.find('#' + name + 'template').clone(true); // change ID to avoid collisions (one ID should really be unique) return $element.prop('id', name); }; @@ -879,8 +1357,8 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name UiHelper * @class */ - var UiHelper = (function () { - var me = {}; + const UiHelper = (function () { + const me = {}; /** * handle history (pop) state changes @@ -894,7 +1372,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ function historyChange(event) { - var currentLocation = Helper.baseUri(); + let currentLocation = Helper.baseUri(); if (event.originalEvent.state === null && // no state object passed event.target.location.href === currentLocation && // target location is home page window.location.href === currentLocation // and we are not already on the home page @@ -928,10 +1406,9 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.isVisible = function($element) { - var elementTop = $element.offset().top; - var viewportTop = $(window).scrollTop(); - var viewportBottom = viewportTop + $(window).height(); - + let elementTop = $element.offset().top, + viewportTop = $(window).scrollTop(), + viewportBottom = viewportTop + $(window).height(); return elementTop > viewportTop && elementTop < viewportBottom; }; @@ -948,12 +1425,12 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback) { - var $body = $('html, body'), + let $body = $('html, body'), margin = 50, - callbackCalled = false; + callbackCalled = false, + dest = 0; - //calculate destination place - var dest = 0; + // calculate destination place // if it would scroll out of the screen at the bottom only scroll it as // far as the screen can go if ($element.offset().top > $(document).height() - $(window).height()) { @@ -1028,25 +1505,23 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name Alert * @class */ - var Alert = (function () { - var me = {}; + const Alert = (function () { + const me = {}; - var $errorMessage, + let $errorMessage, $loadingIndicator, $statusMessage, - $remainingTime; + $remainingTime, + currentIcon, + customHandler; - var currentIcon; - - var alertType = [ - 'loading', // not in bootstrap, but using a good value here - 'info', // status icon + const alertType = [ + 'loading', // not in bootstrap CSS, but using a plausible value here + 'info', // status icon 'warning', // not used yet - 'danger' // error icon + 'danger' // error icon ]; - var customHandler; - /** * forwards a request to the i18n module and shows the element * @@ -1073,7 +1548,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // pass to custom handler if defined if (typeof customHandler === 'function') { - var handlerResult = customHandler(alertType[id], $element, args, icon); + let handlerResult = customHandler(alertType[id], $element, args, icon); if (handlerResult === true) { // if it returns true, skip own handler return; @@ -1089,7 +1564,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { if (icon !== null && // icon was passed icon !== currentIcon[id] // and it differs from current icon ) { - var $glyphIcon = $element.find(':first'); + let $glyphIcon = $element.find(':first'); // remove (previous) icon $glyphIcon.removeClass(currentIcon[id]); @@ -1127,7 +1602,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.showStatus = function(message, icon) { - console.info('status shown: ', message); handleNotification(1, $statusMessage, message, icon); }; @@ -1144,7 +1618,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.showError = function(message, icon) { - console.error('error message shown: ', message); handleNotification(3, $errorMessage, message, icon); }; @@ -1159,7 +1632,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.showRemaining = function(message) { - console.info('remaining message shown: ', message); handleNotification(1, $remainingTime, message); }; @@ -1175,10 +1647,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ me.showLoading = function(message, icon) { - if (typeof message !== 'undefined' && message !== null) { - console.info('status changed: ', message); - } - // default message text if (typeof message === 'undefined') { message = 'Loading…'; @@ -1278,10 +1746,10 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name PasteStatus * @class */ - var PasteStatus = (function () { - var me = {}; + const PasteStatus = (function () { + const me = {}; - var $pasteSuccess, + let $pasteSuccess, $pasteUrl, $remainingTime, $shortenButton; @@ -1352,25 +1820,25 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * * @name PasteStatus.showRemainingTime * @function - * @param {object} pasteMetaData + * @param {Paste} paste */ - me.showRemainingTime = function(pasteMetaData) + me.showRemainingTime = function(paste) { - if (pasteMetaData.burnafterreading) { + if (paste.isBurnAfterReadingEnabled()) { // display paste "for your eyes only" if it is deleted // the paste has been deleted when the JSON with the ciphertext // has been downloaded - Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again."); + Alert.showRemaining('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) { + } else if (paste.getTimeToLive() > 0) { // display paste expiration - var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time), + let expiration = Helper.secondsToHuman(paste.getTimeToLive()), expirationLabel = [ 'This document will expire in %d ' + expiration[1] + '.', 'This document will expire in %d ' + expiration[1] + 's.' @@ -1427,14 +1895,13 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name Prompt * @class */ - var Prompt = (function () { - var me = {}; + const Prompt = (function () { + const me = {}; - var $passwordDecrypt, + let $passwordDecrypt, $passwordForm, - $passwordModal; - - var password = ''; + $passwordModal, + password = ''; /** * submit a password in the modal dialog @@ -1551,15 +2018,14 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name Editor * @class */ - var Editor = (function () { - var me = {}; + const Editor = (function () { + const me = {}; - var $editorTabs, + let $editorTabs, $messageEdit, $messagePreview, - $message; - - var isPreview = false; + $message, + isPreview = false; /** * support input of tab character @@ -1571,13 +2037,13 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { */ function supportTabs(event) { - var keyCode = event.keyCode || event.which; + const keyCode = event.keyCode || event.which; // tab was pressed if (keyCode === 9) { // get caret position & selection - var val = this.value, - start = this.selectionStart, - end = this.selectionEnd; + const 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 @@ -1635,7 +2101,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // show preview PasteViewer.setText($message.val()); if (AttachmentViewer.hasAttachmentData()) { - var attachmentData = AttachmentViewer.getAttachmentData() || AttachmentViewer.getAttachmentLink().attr('href'); + let attachmentData = AttachmentViewer.getAttachmentData() || AttachmentViewer.getAttachmentLink().attr('href'); AttachmentViewer.handleAttachmentPreview(AttachmentViewer.getAttachmentPreview(), attachmentData); } PasteViewer.run(); @@ -1767,15 +2233,14 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name PasteViewer * @class */ - var PasteViewer = (function () { - var me = {}; + const PasteViewer = (function () { + const me = {}; - var $placeholder, + let $placeholder, $prettyMessage, $prettyPrint, - $plainText; - - var text, + $plainText, + text, format = 'plaintext', isDisplayed = false, isChanged = true; // by default true as nothing was parsed yet @@ -1795,16 +2260,16 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } // escape HTML entities, link URLs, sanitize - var escapedLinkedText = Helper.urls2links( + const escapedLinkedText = Helper.urls2links( $('').text(text).html() - ), - sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText); + ), + sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText); $plainText.html(sanitizedLinkedText); $prettyPrint.html(sanitizedLinkedText); switch (format) { case 'markdown': - var converter = new showdown.Converter({ + const converter = new showdown.Converter({ strikethrough: true, tables: true, tablesHeaderId: true, @@ -1813,7 +2278,9 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { }); // let showdown convert the HTML and sanitize HTML *afterwards*! $plainText.html( - DOMPurify.sanitize(converter.makeHtml(text)) + DOMPurify.sanitize( + converter.makeHtml(text) + ) ); // add table classes from bootstrap css $plainText.find('table').addClass('table-condensed table-bordered'); @@ -1969,7 +2436,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { me.hide = function() { if (!isDisplayed) { - console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.'); + return; } $plainText.addClass('hidden'); @@ -2025,17 +2492,17 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { * @name AttachmentViewer * @class */ - var AttachmentViewer = (function () { - var me = {}; + const AttachmentViewer = (function () { + const me = {}; - var $attachmentLink; - var $attachmentPreview; - var $attachment; - var attachmentData; - var file; - var $fileInput; - var $dragAndDropFileName; - var attachmentHasPreview = false; + let $attachmentLink, + $attachmentPreview, + $attachment, + attachmentData, + file, + $fileInput, + $dragAndDropFileName, + attachmentHasPreview = false; /** * sets the attachment but does not yet show it @@ -2054,24 +2521,21 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { // data URI format: data:[][;base64], // position in data URI string of where data begins - var base64Start = attachmentData.indexOf(',') + 1; + const base64Start = attachmentData.indexOf(',') + 1; // position in data URI string of where mediaType ends - var mediaTypeEnd = attachmentData.indexOf(';'); + const mediaTypeEnd = attachmentData.indexOf(';'); // extract mediaType - var mediaType = attachmentData.substring(5, mediaTypeEnd); + const mediaType = attachmentData.substring(5, mediaTypeEnd); // extract data and convert to binary - var decodedData = Base64.atob(attachmentData.substring(base64Start)); + const decodedData = atob(attachmentData.substring(base64Start)); // Transform into a Blob - var decodedDataLength = decodedData.length; - var buf = new Uint8Array(decodedDataLength); - - for (var i = 0; i < decodedDataLength; i++) { + const buf = new Uint8Array(decodedData.length); + for (let i = 0; i < decodedData.length; ++i) { buf[i] = decodedData.charCodeAt(i); } - - var blob = new window.Blob([ buf ], { type: mediaType }); + const blob = new window.Blob([ buf ], { type: mediaType }); navigator.msSaveBlob(blob, fileName); }); } else { @@ -2188,7 +2652,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { if (!$attachment.length) { return false; } - var link = $attachmentLink.prop('href'); + const link = $attachmentLink.prop('href'); return (typeof link !== 'undefined' && link !== ''); }; @@ -2260,7 +2724,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { return; } - var fileReader = new FileReader(); + const fileReader = new FileReader(); if (loadedFile === undefined) { loadedFile = $fileInput[0].files[0]; $dragAndDropFileName.text(''); @@ -2271,7 +2735,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { file = loadedFile; fileReader.onload = function (event) { - var dataURL = event.target.result; + const dataURL = event.target.result; attachmentData = dataURL; if (Editor.isPreview()) { @@ -2293,7 +2757,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { me.handleAttachmentPreview = function ($targetElement, data) { if (data) { // source: https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL() - var mimeType = data.slice( + const mimeType = data.slice( data.indexOf('data:') + 5, data.indexOf(';base64,') ); @@ -2339,7 +2803,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { } // Fallback for browsers, that don't support the vh unit - var clientHeight = $(window).height(); + const clientHeight = $(window).height(); $targetElement.html( $(document.createElement('embed')) @@ -2366,18 +2830,18 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) { return; } - var ignoreDragDrop = function(event) { + const ignoreDragDrop = function(event) { event.stopPropagation(); event.preventDefault(); }; - var drop = function(event) { - var evt = event.originalEvent; + const drop = function(event) { + const evt = event.originalEvent; evt.stopPropagation(); evt.preventDefault(); if ($fileInput) { - var file = evt.dataTransfer.files[0]; + const file = evt.dataTransfer.files[0]; //Clear the file input: $fileInput.wrap('