/*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) const app = new Vue({ el: '#app', data: { previousPastes: [], displayBottomToolBar: false, openPreviousPastesMenu: false, readerMode: false, isUploading: 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 () { debugger; 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) { let readableModeContent = document.getElementById('readable-paste-content') let fontSize = window.getComputedStyle(readableModeContent, null).getPropertyValue('font-size'); amount = amount || 5; readableModeContent.style.fontSize = (parseFloat(fontSize) + amount) + "px"; }, decreaseFontSize: function () { this.increaseFontSize(-5); }, formatEmail: (email) => { return "mailto:" + email.replace('__AT__', '@'); }, forceLoad: (link) => { window.location = link; window.location.reload(); }, handleClone: function () { document.querySelector('.submit-form').style.display = "inherit"; document.querySelector('.paste-form').style.display = "none"; let title = document.querySelector('h1'); if (title) { title.style.display = "none"; } let 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: () => { document.querySelector('.submit-form').style.display = "none"; document.querySelector('.paste-form').style.display = "inherit"; document.querySelector('h1').style.display = "inherit"; }, handleUpload: (files) => { try { app.isUploading = true; zerobin.upload(files); } catch (e) { zerobin.message('danger', '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( '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: () => { var pasteContent = zerobin.getPasteContent(); let 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); }); }, /** 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: () => { 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, 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((node) => node.disabled = false); zerobin.message('danger', ('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('danger', 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( 'danger', 'Paste could not be saved. Please try again later.', 'Error'); } }).catch(function (error) { form.forEach((node) => node.disabled = false); app.isLoading = false zerobin.message( 'danger', 'Paste could not be saved. Please try again later.', 'Error'); }); }); } catch (err) { form.forEach((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((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 { 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) { 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 '; } 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
parent for the because IE requires the link be processed by it's HTML parser for the URL to be parsed. */ parseUrl: (function () { var div = document.createElement('div'); div.innerHTML = ""; return function (url) { div.firstChild.href = url; div.innerHTML = div.innerHTML; return div.firstChild; }; })(), getPasteId: function (url) { var loc = url ? zerobin.parseUrl(url) : window.location; return loc.pathname.replace(/\/|paste/g, ''); }, getPasteOwnerKey: function (url) { var loc = url ? zerobin.parseUrl(url) : window.location; return (new URLSearchParams(loc.search)).get("owner_key"); }, getPasteKey: function (url) { var loc = url ? zerobin.parseUrl(url) : window.location; return loc.hash.replace('#', '').replace(/(\?|&).*$/, ''); }, /** Return the paste content stripted from any code coloration */ getPasteContent: function () { var copy = ''; document.querySelectorAll("#paste-content li").forEach((node) => { copy = copy + node.textContent.replace(/[\u00a0]+/g, ' ') + '\n'; }) if (copy === '') { copy = document.querySelector("#paste-content").textContent; } return copy; }, /** Return an approximate estimate of the number of bytes in a text */ count: function (text, options) { // Set option defaults var crlf = /(\r?\n|\r)/g; var whitespace = /(\r?\n|\r|\s+)/g; options = options || {}; options.lineBreaks = options.lineBreaks || 1; var length = text.length, nonAscii = length - text.replace(/[\u0100-\uFFFF]/g, '').length, lineBreaks = length - text.replace(crlf, '').length; return length + nonAscii + Math.max(0, options.lineBreaks * (lineBreaks - 1)); }, /** Create a message, style it and insert it in the alert box */ message: function (type, message, title, flush, callback, action) { window.scrollTo(0, 0); if (flush) { app.messages = app.messages.filter((msg) => { msg.type !== type }); } app.messages.push({ title: title, content: message, type: type, action: action || {}, }); callback && callback() }, /** Return a progress bar object */ progressBar: function (selector) { var container = document.querySelector(selector); var bar = { container: container, elem: container.childNodes[0] }; bar.set = function (text, rate) { bar.elem.innerHTML = text; bar.elem.style.width = rate; }; return bar; }, /** Return an integer ranking the probability this text is any kind of source code. */ isCode: function (text) { var code_chars = /[A-Z]{3}[A-Z]+|\.[a-z]|[=:<>{}\[\]$_'"&]| {2}|\t/g; var comments = /(:?\/\*|)|(\/\/|#)(.*?)\n/g; var formating = /[-*=_+]{4,}/; var total = 0; var size = 0; var m = text.match(comments); if (m) { total += text.match(comments).length; } text = text.replace(comments, ''); text.replace(formating, ''); text = text.split('\n'); for (var i = 0; i < text.length; i++) { var line = text[i]; size += line.length; var match = line.replace(formating, '').match(code_chars); if (match) { total += match.length; } } return total * 1000 / size; }, ignoreDrag: function (e) { e.stopPropagation(); e.preventDefault(); }, handleDrop: function (e) { e.preventDefault(); zerobin.upload(e.dataTransfer.files); document.getElementById("content").classList.remove("hover"); }, handlePaste: function (e) { var items = (event.clipboardData || event.originalEvent.clipboardData).items; for (var i = 0; i < items.length; i++) { if (items[i].type.indexOf("image") === 0) { e.preventDefault() return zerobin.upload([items[i].getAsFile()]); } } }, handleDragOver: function (e) { zerobin.ignoreDrag(e); document.getElementById("content").classList.add('hover'); }, handleDragLeave: function (e) { document.getElementById("content").classList.remove("hover"); }, upload: function (files) { let content = document.getElementById('content'); var currentFile = files[0]; var reader = new FileReader(); if (currentFile.type.indexOf('image') == 0) { reader.onload = function (event) { var image = new Image(); image.src = event.target.result; content.value = event.target.result image.onload = function () { var imgWrapper = document.createElement('div'); imgWrapper.classList.add('paste-wrapper'); imgWrapper.appendChild(image) content.style.display = "none"; content.after(imgWrapper); } } reader.readAsDataURL(currentFile); } else { reader.onload = function (event) { content.value = event.target.result content.dispatchEvent(new Event('change')); }; reader.readAsText(currentFile); } } }; /** DECRYPTION: On the display paste page, decrypt and decompress the paste content, add syntax coloration then setup the copy to clipboard button. Also calculate and set the paste visual hash. */ let pasteContent = document.querySelector('#paste-content'); let content = ''; if (pasteContent) { content = pasteContent.textContent.trim(); app.currentPaste.id = zerobin.getPasteId(window.location); } var key = zerobin.getPasteKey(); var error = false; if (content && key) { var form = document.querySelectorAll('input, textarea, select, button'); form.forEach((node) => node.disabled = true); var bar = zerobin.progressBar('.well form .progress'); app.isLoading = true; bar.set('Decrypting paste...', '25%'); zerobin.decrypt(key, content, /* On error*/ function () { app.isLoading = false; zerobin.message('danger', 'Could not decrypt data (Wrong key ?)', 'Error'); }, /* Update progress bar */ () => bar.set('Decompressing...', '45%'), () => bar.set('Base64 decoding...', '65%'), () => bar.set('From bits to string...', '85%'), /* When done */ function (content) { let readerMode = false; if (content.indexOf('data:image') == 0) { // Display Image app.currentPaste.type = "image"; let pasteContent = document.querySelector('#paste-content'); pasteContent.style.display = "none"; var imgWrapper = document.createElement('div'); imgWrapper.classList.add('paste-wrapper'); var img = document.createElement('img'); img.src = content; pasteContent.after(imgWrapper); imgWrapper.appendChild(img); document.querySelectorAll('.btn-clone').forEach((node) => node.style.display = "none") let extension = /data:image\/([^;]+);base64/.exec(content)[1]; app.currentPaste.downloadLink = { name: '0bin_' + document.location.pathname.split('/').pop() + '.' + extension, url: content } } else { app.currentPaste.type = "text" /* Decrypted content goes back to initial container*/ document.querySelector('#paste-content').innerText = content; app.currentPaste.content = content app.currentPaste.downloadLink = { name: '0bin_' + document.location.pathname.split('/').pop() + ".txt", url: "data:text/html;charset=UTF-8," + content } if (app.support.URLSearchParams) { readerMode = (new URLSearchParams(window.location.search)).get('readerMode'); } } bar.set('Code coloration...', '95%'); /* Add a continuation to let the UI redraw */ setTimeout(function () { /** Syntaxic coloration */ if (zerobin.isCode(content) > 100 && !readerMode) { document.getElementById('paste-content').classList.add('linenums'); prettyPrint(); } else { if (content.indexOf('data:image') != 0) { zerobin.message('primary', "The paste did not seem to be code, so it " + "was not colorized. ", '', false, undefined, { message: "Force coloration", callback: app.handleForceColoration }); } } /* Class to switch to paste content style with coloration done */ document.getElementById('paste-content').classList.add('done'); /* Display result */ bar.set('Done', '100%'); app.isLoading = false; form.forEach((node) => node.disabled = false); content = ''; if (readerMode) { app.toggleReaderMode() } }, 100); }); } /* End of "DECRYPTION" */ window.onload = function () { /* Display bottom paste option buttons when needed */ ["keyup", "change"].forEach((event) => { let content = document.getElementById("content"); content.addEventListener(event, () => { let height = parseFloat(getComputedStyle(content, null).height.replace("px", "")) app.displayBottomToolBar = height > 400; }) }) /* Remove expired pasted from history **/ if (app.support.history && app.currentPaste.type === 'not_found') { var paste_id = zerobin.getPasteId(); var keys = zerobin.getLocalStorageURLKeys(); keys.forEach((key, i) => { if (localStorage[key].indexOf(paste_id) !== -1) { localStorage.removeItem(key); return false; } }) } let title = document.querySelector('h1'); if (title) { app.currentPaste.title = title.innerText; } let btcTipAddress = document.querySelector('.btc-tip-address a'); if (btcTipAddress) { app.currentPaste.btcTipAddress = btcTipAddress.innerText; } } /* Display previous pastes */ if (app.support.localStorage) { app.previousPastes = zerobin.getPreviousPastes(); app.currentPaste.ownerKey = localStorage.getItem('zerobinV' + zerobin.version + "#" + zerobin.getPasteId(window.location) + "#owner_key"); } /* Upload file using HTML5 File API */ if (app.support.fileUpload) { // Implements drag & drop upload let content = document.getElementById('content'); content.addEventListener('drop', zerobin.handleDrop); content.addEventListener('paste', zerobin.handlePaste); content.addEventListener('dragover', zerobin.handleDragOver); content.addEventListener('dragleave', zerobin.handleDragLeave); } /* Autofit text area height */ const tx = document.getElementsByTagName('textarea'); for (let i = 0; i < tx.length; i++) { tx[i].setAttribute('style', 'height:' + (tx[i].scrollHeight) + 'px;overflow-y:hidden;'); tx[i].addEventListener("input", OnInput, false); } function OnInput() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; }