/*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) var app = new Vue({ el: '#app', data: { previousPastes: [], displayBottomToolBar: false, openPreviousPastesMenu: false, readerMode: false, isUploading: false, btcCopied: false, currentPaste: { ownerKey: '', id: '', type: '', content: '', downloadLink: {}, title: '', btcTipAddress: '' }, newPaste: { expiration: '1_day', content: '', title: '', btcTipAddress: '' }, 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), URLSearchParams: !!window.URLSearchParams, 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: { toggleReaderMode: function () { if (!this.readerMode) { this.messages = []; if (this.support.URLSearchParams) { var searchParams = new URLSearchParams(window.location.search); searchParams.set('readerMode', 1); window.location.search = searchParams.toString(); } } else { if (this.support.URLSearchParams) { var searchParams = new URLSearchParams(window.location.search); searchParams.delete('readerMode'); window.location.search = searchParams.toString(); } } this.readerMode = !this.readerMode; }, increaseFontSize: function (amount) { var readableModeContent = document.getElementById('readable-paste-content'); var fontSize = window.getComputedStyle(readableModeContent, null).getPropertyValue('font-size'); amount = amount || 5; readableModeContent.style.fontSize = (parseFloat(fontSize) + amount) + "px"; }, decreaseFontSize: function () { this.increaseFontSize(-5); }, formatEmail: function (email) { return "mailto:" + email.replace('__AT__', '@'); }, forceLoad: function (link) { window.location = link; window.location.reload(); }, handleClone: function () { document.querySelector('.submit-form').style.display = "inherit"; document.querySelector('.paste-form').style.display = "none"; var title = document.querySelector('h1'); if (title) { title.style.display = "none"; } var content = document.getElementById('content'); content.value = zerobin.getPasteContent(); content.dispatchEvent(new Event('change')); this.newPaste.title = this.currentPaste.title; this.newPaste.btcTipAddress = this.currentPaste.btcTipAddress; }, handleCancelClone: function () { document.querySelector('.submit-form').style.display = "none"; document.querySelector('.paste-form').style.display = "inherit"; document.querySelector('h1').style.display = "inherit"; }, handleUpload: function (files) { try { app.isUploading = true; zerobin.upload(files); } catch (e) { zerobin.message('danger', 'Could no upload the file', 'Error'); } app.isUploading = false; }, handleForceColoration: function (e) { var content = document.getElementById('paste-content'); content.classList.add('linenums'); e.target.innerHTML = 'Applying coloration'; prettyPrint(); e.target.parentNode.remove() }, handleSendByEmail: function (e) { window.location = 'mailto:friend@example.com?body=' + window.location.toString(); }, handleDeletePaste: function () { 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) { window.location = "/"; } else { form.forEach(function (node) { node.disabled = false; }); app.isLoading = false zerobin.message( 'danger', 'Paste could not be deleted. Please try again later.', 'Error'); } app.isLoading = false; }).catch(function (error) { zerobin.message( 'danger', 'Paste could not be delete. Please try again later.', 'Error'); app.isLoading = false; }); } }, copyToClipboard: function () { var pasteContent = zerobin.getPasteContent(); var promise = navigator.clipboard.writeText(pasteContent); promise.then(function () { zerobin.message('primary', 'The paste is now in your clipboard', '', true); }, function (err) { zerobin.message('danger', 'The paste could not be copied', '', true); }); }, copyBTCAdressToClipboard: function () { var promise = navigator.clipboard.writeText(this.currentPaste.btcTipAddress); app.btcCopied = true; promise.then(function () { if (app.btcCopied) { clearTimeout(app.btcCopied); } app.btcCopied = setTimeout(function () { app.btcCopied = false; }, 3000) }, function (err) { zerobin.message('danger', 'The BTC addresse could not be copied', '', true); }); }, compressImage: function (base64) { var canvas = document.createElement('canvas') var img = document.createElement('img') return new Promise(function (resolve, reject) { img.onload = function () { var width = img.width; var height = img.height; var biggest = width > height ? width : height; var maxHeight = height; var maxWidth = width; if (width > height) { if (width > maxWidth) { height = Math.round((height *= maxWidth / width)); width = maxWidth; } } else { if (height > maxHeight) { width = Math.round((width *= maxHeight / height)); height = maxHeight; } } canvas.width = width; canvas.height = height; var ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/jpeg', 0.7)); } img.onerror = function (err) { reject(err); } img.src = base64; }) }, /** 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: function () { var paste = document.querySelector('textarea').value; if (paste.trim()) { var form = document.querySelectorAll('input, textarea, select, button'); form.forEach(function (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); var promise = new Promise(function (resolve, reject) { resolve(paste); }); // noop to avoid branching if (paste.indexOf('data:image') == 0) { promise = app.compressImage(paste); } promise.then(function (base64) { zerobin.encrypt(key, base64, function () { bar.set('Encoding to base64...', '45%') }, function () { bar.set('Compressing...', '65%') }, function () { 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, title: app.newPaste.title, btcTipAddress: app.newPaste.btcTipAddress }; 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(function (node) { node.disabled = false; }); zerobin.message('danger', ('The file was ' + readableFsize + 'KB after encryption. 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(function (data) { if (data.status === 'error') { zerobin.message('danger', data.message, 'Error'); form.forEach(function (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(function (node) { node.disabled = false }); app.isLoading = false; zerobin.message( 'danger', 'Paste could not be saved. Please try again later.', 'Error'); } }).catch(function (error) { form.forEach(function (node) { node.disabled = false; }); app.isLoading = false; zerobin.message( 'danger', 'Paste could not be saved. Please try again later.', 'Error'); }); }) }), function (err) { debugger; form.forEach(function (node) { node.disabled = false; }); app.isLoading = false; zerobin.message('danger', 'Paste could not be encrypted. Aborting.', 'Error'); }; } catch (err) { form.forEach(function (node) { node.disabled = false; }); app.isLoading = false; zerobin.message('danger', '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(function (node) { node.disabled = true }); app.isLoading = false; zerobin.message('danger', '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 { 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) { debugger; 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 '; } var 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