/*global sjcl:true, jQuery:true, lzw:true, zerobin:true, prettyPrint:true */ /* This file has been migrated away from jQuery, to Vue. Because of the way the code base used to be, a lot of the operations are still using imperative DOM manipulation instead of the Vue declarative style. We haven't had the time to rewrite it completly and it's a bit of a mixed bag at the moment. */ /* Start random number generator seeding ASAP */ sjcl.random.startCollectors(); // Vue template syntax conflicts with bottle template syntax Vue.options.delimiters = ['{%', '%}']; // Force focus for textarea (firefox hack) setTimeout(function () { document.getElementById('content').focus() }, 100) // Parse obfuscaded emails and make them usable const menu = new Vue({ el: "#menu-top", methods: { } }) const app = new Vue({ el: '#app', data: { previousPastes: [], downloadLink: {}, displayBottomToolBar: false, openPreviousPastesMenu: false, isUploading: false, currentPaste: { ownerKey: '', id: '', type: '', }, newPaste: { expiration: '1_day', content: '', }, messages: [], /** Check for browser support of the named featured. Store the result and add a class to the html tag with the result */ support: { clipboard: !!(isSecureContext && navigator.clipboard && navigator.clipboard.writeText), localStorage: (function () { var val = !!(localStorage); document.querySelector('html').classList.add((val ? '' : 'no-') + 'local-storage'); return val; })(), history: (function () { var val = !!(window.history && history.pushState); document.querySelector('html').classList.add((val ? '' : 'no-') + 'history'); return val; })(), fileUpload: (function () { var w = window; var val = !!(w.File && w.FileReader && w.FileList && w.Blob); document.querySelector('html').classList.add((val ? '' : 'no-') + 'file-upload'); return val; })() }, isLoading: false }, methods: { formatEmail: (email) => { return "mailto:" + email.replace('__AT__', '@'); }, forceLoad: (link) => { window.location = link; window.location.reload(); }, handleClone: () => { document.querySelector('.submit-form').style.display = "inherit"; document.querySelector('.paste-form').style.display = "none"; let content = document.getElementById('content'); content.value = zerobin.getPasteContent(); content.dispatchEvent(new Event('change')); }, handleCancelClone: () => { document.querySelector('.submit-form').style.display = "none"; document.querySelector('.paste-form').style.display = "inherit"; }, handleUpload: (files) => { try { app.isUploading = true; zerobin.upload(files); } catch (e) { zerobin.message('error', 'Could no upload the file', 'Error'); } app.isUploading = false; }, handleForceColoration: (e) => { let content = document.getElementById('paste-content'); content.classList.add('linenums'); e.target.innerHTML = 'Applying coloration'; prettyPrint(); e.target.parentNode.remove() }, handleSendByEmail: (e) => { window.location = 'mailto:friend@example.com?body=' + window.location.toString(); }, handleDeletePaste: () => { if (window.confirm("Delete this paste?")) { app.isLoading = true; bar.set('Deleting paste...', '50%'); fetch('/paste/' + app.currentPaste.id, { method: "DELETE", body: new URLSearchParams({ "owner_key": app.currentPaste.ownerKey }) }).then(function (response) { if (response.ok) { app.forceLoad("/"); } else { form.forEach((node) => node.disabled = false); app.isLoading = false zerobin.message( 'error', 'Paste could not be deleted. Please try again later.', 'Error'); } app.isLoading = false; }).catch(function (error) { zerobin.message( 'error', 'Paste could not be delete. Please try again later.', 'Error'); app.isLoading = false; }); } }, copyToClipboard: () => { var pasteContent = zerobin.getPasteContent(); let promise = navigator.clipboard.writeText(pasteContent); promise.then(function () { zerobin.message('info', 'The paste is now in your clipboard', '', true); }, function (err) { zerobin.message('error', 'The paste could not be copied', '', true); }); }, /** On the create paste page: On click on the send button, compress and encrypt data before posting it using ajax. Then redirect to the address of the newly created paste, adding the key in the hash. */ encryptAndSendPaste: (e) => { var paste = document.querySelector('textarea').value; if (paste.trim()) { var form = document.querySelectorAll('input, textarea, select, button'); form.forEach((node) => node.disabled = true); // set up progress bar var bar = zerobin.progressBar('form.well .progress'); app.isLoading = true; bar.set('Converting paste to bits...', '25%'); /* Encode, compress, encrypt and send the paste then redirect the user to the new paste. We ensure a loading animation is updated during the process by passing callbacks. */ try { var key = zerobin.makeKey(256); zerobin.encrypt(key, paste, () => bar.set('Encoding to base64...', '45%'), () => bar.set('Compressing...', '65%'), () => bar.set('Encrypting...', '85%'), /* This block deals with sending the data, redirection or error handling */ function (content) { bar.set('Sending...', '95%'); var data = { content: content, expiration: app.newPaste.expiration }; var sizebytes = zerobin.count(JSON.stringify(data)); var oversized = sizebytes > zerobin.max_size; // 100kb - the others header information var readableFsize = Math.round(sizebytes / 1024); var readableMaxsize = Math.round(zerobin.max_size / 1024); if (oversized) { app.isLoading = false form.forEach((node) => node.disabled = false); zerobin.message('error', ('The encrypted file was ' + readableFsize + 'KB. You have reached the maximum size limit of ' + readableMaxsize + 'KB.'), 'Warning!', true); return; } fetch('/paste/create', { method: "POST", body: new URLSearchParams(data) }).then(function (response) { if (response.ok) { bar.set('Redirecting to new paste...', '100%'); response.json().then((data) => { if (data.status === 'error') { zerobin.message('error', data.message, 'Error'); form.forEach((node) => node.disabled = false); app.isLoading = false } else { if (app.support.localStorage) { zerobin.storePaste('/paste/' + data.paste + "?owner_key=" + data.owner_key + '#' + key); } window.location = ('/paste/' + data.paste + '#' + key); } }) } else { form.forEach((node) => node.disabled = false); app.isLoading = false zerobin.message( 'error', 'Paste could not be saved. Please try again later.', 'Error'); } }).catch(function (error) { form.forEach((node) => node.disabled = false); app.isLoading = false zerobin.message( 'error', 'Paste could not be saved. Please try again later.', 'Error'); }); }); } catch (err) { form.forEach((node) => node.disabled = false); app.isLoading = false zerobin.message('error', 'Paste could not be encrypted. Aborting.', 'Error'); } } } } }) /**************************** **** 0bin utilities **** ****************************/ window.zerobin = { /** Base64 + compress + encrypt, with callbacks before each operation, and all of them are executed in a timed continuation to give a change to the UI to respond. */ version: '0.1.1', encrypt: function (key, content, toBase64Callback, compressCallback, encryptCallback, doneCallback) { setTimeout(function () { content = sjcl.codec.utf8String.toBits(content); if (toBase64Callback) { toBase64Callback(); } setTimeout(function () { content = sjcl.codec.base64.fromBits(content); if (compressCallback) { compressCallback(); } setTimeout(function () { // content = lzw.compress(content); // Create a bug with JPG if (encryptCallback) { encryptCallback(); } setTimeout(function () { try { content = sjcl.encrypt(key, content); } catch (e) { document.querySelectorAll('input, textarea, select, button').forEach((node) => node.disabled = true); app.isLoading = false; zerobin.message('error', 'Paste could not be encrypted. Aborting.', 'Error'); } if (doneCallback) { doneCallback(content); } }, 50); }, 50); }, 50); }, 50); }, /** Base64 decoding + uncompress + decrypt, with callbacks before each operation, and all of them are executed in a timed continuation to give a change to the UI to respond. This is where using a library to fake synchronicity could start to be useful, this code is starting be difficult to read. If anyone read this and got a suggestion, by all means, speak your mind. */ decrypt: function (key, content, errorCallback, uncompressCallback, fromBase64Callback, toStringCallback, doneCallback) { /* Decrypt */ setTimeout(function () { try { content = sjcl.decrypt(key, content); if (uncompressCallback) { uncompressCallback(); } /* Decompress */ setTimeout(function () { try { content = lzw.decompress(content); if (fromBase64Callback) { fromBase64Callback(); } /* From base 64 to bits */ setTimeout(function () { try { content = sjcl.codec.base64.toBits(content); if (toStringCallback) { toStringCallback(); } /* From bits to string */ setTimeout(function () { try { content = sjcl.codec.utf8String.fromBits(content); if (doneCallback) { doneCallback(content); } } catch (err) { errorCallback(err); } }, 50); /* "End of from bits to string" */ } catch (err) { errorCallback(err); } }, 50); /* End of "from base 64 to bits" */ } catch (err) { errorCallback(err); } }, 50); /* End of "decompress" */ } catch (err) { errorCallback(err); } }, 50); /* End of "decrypt" */ }, /** Create a random base64-like string long enought to be suitable as an encryption key */ makeKey: function (entropy) { entropy = Math.ceil(entropy / 6) * 6; /* non-6-multiple produces same-length base64 */ var key = sjcl.bitArray.clamp( sjcl.random.randomWords(Math.ceil(entropy / 32), 0), entropy); return sjcl.codec.base64.fromBits(key, 0).replace(/\=+$/, '').replace(/\//, '-'); }, getFormatedDate: function (date) { date = date || new Date(); return ((date.getMonth() + 1) + '-' + date.getDate() + '-' + date.getFullYear()); }, getFormatedTime: function (date) { date = date || new Date(); var h = date.getHours(), m = date.getMinutes(), s = date.getSeconds(); if (h < 10) { h = "0" + h; } if (m < 10) { m = "0" + m; } if (s < 10) { s = "0" + s; } return h + ":" + m + ":" + s; }, numOrdA: function (a, b) { return (a - b); }, /** Return a reverse sorted list of all the keys in local storage that are prefixed with with the passed version (default being this lib version) */ getLocalStorageURLKeys: function () { var version = 'zerobinV' + zerobin.version; var keys = []; for (var key in localStorage) { if (key.indexOf(version) !== -1 && key.indexOf("owner_key") === -1) { keys.push(key); } } keys.sort(); keys.reverse(); return keys; }, /** Store the paste of a URL in local storate, with a storage format version prefix and the paste date as the key */ storePaste: function (url, date) { date = date || new Date(); date = (date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + zerobin.getFormatedTime(date)); var keys = zerobin.getLocalStorageURLKeys(); if (localStorage.length > 19) { void localStorage.removeItem(keys[19]); } localStorage.setItem('zerobinV' + zerobin.version + "#" + date, url); localStorage.setItem('zerobinV' + zerobin.version + "#" + zerobin.getPasteId(url) + "#owner_key", zerobin.getPasteOwnerKey(url)); }, /** Return a list of the previous paste url with the creation date If the paste is from today, date format should be "at hh:ss", else it should be "the mm-dd-yyy" */ getPreviousPastes: function () { var keys = zerobin.getLocalStorageURLKeys(), today = zerobin.getFormatedDate(); return keys.map(function (key, i) { var pasteDateTime = key.replace(/^[^#]+#/, ''); var displayDate = pasteDateTime.match(/^(\d+)-(\d+)-(\d+)\s/); displayDate = displayDate[2] + '-' + displayDate[3] + '-' + displayDate[1]; var prefix = 'the '; if (displayDate === today) { displayDate = pasteDateTime.split(' ')[1]; prefix = 'at '; } let link = localStorage.getItem(key); return { displayDate: displayDate, prefix: prefix, // The owner key is stored in the URL, but we don't want the user // to see it link: link.replace(/\?[^#]+#/, '#'), isCurrent: link.replace(/\?[^?]+/, '') === window.location.pathname }; }); }, /** Return a link object with the URL as href so you can extract host, protocol, hash, etc. This function use a closure to store a