zerobin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. /**
  2. * ZeroBin
  3. *
  4. * a zero-knowledge paste bin
  5. *
  6. * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
  7. * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
  8. * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
  9. * @version 0.19
  10. */
  11. // Immediately start random number generator collector.
  12. sjcl.random.startCollectors();
  13. /**
  14. * Converts a duration (in seconds) into human readable format.
  15. *
  16. * @param int seconds
  17. * @return string
  18. */
  19. function secondsToHuman(seconds)
  20. {
  21. if (seconds<60) { var v=Math.floor(seconds); return v+' second'+((v>1)?'s':''); }
  22. if (seconds<60*60) { var v=Math.floor(seconds/60); return v+' minute'+((v>1)?'s':''); }
  23. if (seconds<60*60*24) { var v=Math.floor(seconds/(60*60)); return v+' hour'+((v>1)?'s':''); }
  24. // If less than 2 months, display in days:
  25. if (seconds<60*60*24*60) { var v=Math.floor(seconds/(60*60*24)); return v+' day'+((v>1)?'s':''); }
  26. var v=Math.floor(seconds/(60*60*24*30)); return v+' month'+((v>1)?'s':'');
  27. }
  28. /**
  29. * Converts an associative array to an encoded string
  30. * for appending to the anchor.
  31. *
  32. * @param object associative_array Object to be serialized
  33. * @return string
  34. */
  35. function hashToParameterString(associativeArray)
  36. {
  37. var parameterString = ""
  38. for (key in associativeArray)
  39. {
  40. if( parameterString === "" )
  41. {
  42. parameterString = encodeURIComponent(key);
  43. parameterString += "=" + encodeURIComponent(associativeArray[key]);
  44. } else {
  45. parameterString += "&" + encodeURIComponent(key);
  46. parameterString += "=" + encodeURIComponent(associativeArray[key]);
  47. }
  48. }
  49. //padding for URL shorteners
  50. parameterString += "&p=p";
  51. return parameterString;
  52. }
  53. /**
  54. * Converts a string to an associative array.
  55. *
  56. * @param string parameter_string String containing parameters
  57. * @return object
  58. */
  59. function parameterStringToHash(parameterString)
  60. {
  61. var parameterHash = {};
  62. var parameterArray = parameterString.split("&");
  63. for (var i = 0; i < parameterArray.length; i++) {
  64. //var currentParamterString = decodeURIComponent(parameterArray[i]);
  65. var pair = parameterArray[i].split("=");
  66. var key = decodeURIComponent(pair[0]);
  67. var value = decodeURIComponent(pair[1]);
  68. parameterHash[key] = value;
  69. }
  70. return parameterHash;
  71. }
  72. /**
  73. * Get an associative array of the parameters found in the anchor
  74. *
  75. * @return object
  76. **/
  77. function getParameterHash()
  78. {
  79. var hashIndex = window.location.href.indexOf("#");
  80. if (hashIndex >= 0) {
  81. return parameterStringToHash(window.location.href.substring(hashIndex + 1));
  82. } else {
  83. return {};
  84. }
  85. }
  86. /**
  87. * Compress a message (deflate compression). Returns base64 encoded data.
  88. *
  89. * @param string message
  90. * @return base64 string data
  91. */
  92. function compress(message) {
  93. return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
  94. }
  95. /**
  96. * Decompress a message compressed with compress().
  97. */
  98. function decompress(data) {
  99. return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
  100. }
  101. /**
  102. * Compress, then encrypt message with key.
  103. *
  104. * @param string key
  105. * @param string message
  106. * @return encrypted string data
  107. */
  108. function zeroCipher(key, message) {
  109. return sjcl.encrypt(key,compress(message));
  110. }
  111. /**
  112. * Decrypt message with key, then decompress.
  113. *
  114. * @param key
  115. * @param encrypted string data
  116. * @return string readable message
  117. */
  118. function zeroDecipher(key, data) {
  119. return decompress(sjcl.decrypt(key,data));
  120. }
  121. /**
  122. * @return the current script location (without search or hash part of the URL).
  123. * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
  124. */
  125. function scriptLocation() {
  126. var scriptLocation = window.location.href.substring(0,window.location.href.length
  127. - window.location.search.length - window.location.hash.length);
  128. var hashIndex = scriptLocation.indexOf("#");
  129. if (hashIndex !== -1) {
  130. scriptLocation = scriptLocation.substring(0, hashIndex)
  131. }
  132. return scriptLocation
  133. }
  134. /**
  135. * @return the paste unique identifier from the URL
  136. * eg. 'c05354954c49a487'
  137. */
  138. function pasteID() {
  139. return window.location.search.substring(1);
  140. }
  141. function htmlEntities(str) {
  142. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  143. }
  144. /**
  145. * Set text of a DOM element (required for IE)
  146. * This is equivalent to element.text(text)
  147. * @param object element : a DOM element.
  148. * @param string text : the text to enter.
  149. */
  150. function setElementText(element, text) {
  151. // For IE<10.
  152. if ($('#oldienotice').is(":visible")) {
  153. // IE<10 does not support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
  154. var html = htmlEntities(text).replace(/\n/ig,"\r\n<br>");
  155. element.html('<pre>'+html+'</pre>');
  156. }
  157. // for other (sane) browsers:
  158. else {
  159. element.text(text);
  160. }
  161. }
  162. /**
  163. * Show decrypted text in the display area, including discussion (if open)
  164. *
  165. * @param string key : decryption key
  166. * @param array comments : Array of messages to display (items = array with keys ('data','meta')
  167. */
  168. function displayMessages(key, comments) {
  169. try { // Try to decrypt the paste.
  170. var cleartext = zeroDecipher(key, comments[0].data);
  171. } catch(err) {
  172. $('#cleartext').addClass('hidden');
  173. $('#prettymessage').addClass('hidden');
  174. $('#clonebutton').addClass('hidden');
  175. showError('Could not decrypt data (Wrong key ?)');
  176. return;
  177. }
  178. setElementText($('#cleartext'), cleartext);
  179. setElementText($('#prettyprint'), cleartext);
  180. // Convert URLs to clickable links.
  181. urls2links($('#cleartext'));
  182. urls2links($('#prettyprint'));
  183. if (typeof prettyPrint == 'function') prettyPrint();
  184. // Display paste expiration.
  185. if (comments[0].meta.expire_date) $('#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').removeClass('hidden');
  186. if (comments[0].meta.burnafterreading) {
  187. $('#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.').removeClass('hidden');
  188. $('#clonebutton').addClass('hidden'); // Discourage cloning (as it can't really be prevented).
  189. }
  190. // If the discussion is opened on this paste, display it.
  191. if (comments[0].meta.opendiscussion) {
  192. $('#comments').html('');
  193. // For each comment.
  194. for (var i = 1; i < comments.length; i++) {
  195. var comment=comments[i];
  196. var cleartext="[Could not decrypt comment ; Wrong key ?]";
  197. try {
  198. cleartext = zeroDecipher(key, comment.data);
  199. } catch(err) { }
  200. var place = $('#comments');
  201. // If parent comment exists, display below (CSS will automatically shift it right.)
  202. var cname = '#comment_'+comment.meta.parentid
  203. // If the element exists in page
  204. if ($(cname).length) {
  205. place = $(cname);
  206. }
  207. var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
  208. + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
  209. + '<button onclick="open_reply($(this),\'' + comment.meta.commentid + '\');return false;" class="btn btn-default">Reply</button>'
  210. + '</div></article>');
  211. setElementText(divComment.find('div.commentdata'), cleartext);
  212. // Convert URLs to clickable links in comment.
  213. urls2links(divComment.find('div.commentdata'));
  214. divComment.find('span.nickname').html('<i>(Anonymous)</i>');
  215. // Try to get optional nickname:
  216. try {
  217. divComment.find('span.nickname').text(zeroDecipher(key, comment.meta.nickname));
  218. } catch(err) { }
  219. divComment.find('span.commentdate').text(' ('+(new Date(comment.meta.postdate*1000).toString())+')').attr('title','CommentID: ' + comment.meta.commentid);
  220. // If an avatar is available, display it.
  221. if (comment.meta.vizhash) {
  222. divComment.find('span.nickname').before('<img src="' + comment.meta.vizhash + '" class="vizhash" title="Anonymous avatar (Vizhash of the IP address)" />');
  223. }
  224. place.append(divComment);
  225. }
  226. $('#comments').append('<div class="comment"><button onclick="open_reply($(this),\'' + pasteID() + '\');return false;" class="btn btn-default">Add comment</button></div>');
  227. $('#discussion').removeClass('hidden');
  228. }
  229. }
  230. /**
  231. * Open the comment entry when clicking the "Reply" button of a comment.
  232. * @param object source : element which emitted the event.
  233. * @param string commentid = identifier of the comment we want to reply to.
  234. */
  235. function open_reply(source, commentid) {
  236. $('div.reply').remove(); // Remove any other reply area.
  237. source.after('<div class="reply">'
  238. + '<input type="text" id="nickname" class="form-control" title="Optional nickname..." value="Optional nickname..." />'
  239. + '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>'
  240. + '<br /><button id="replybutton" onclick="send_comment(\'' + commentid + '\');return false;" class="btn btn-default">Post comment</button>'
  241. + '<div id="replystatus"> </div>'
  242. + '</div>');
  243. $('#nickname').focus(function() {
  244. if ($(this).val() == $(this).attr('title')) {
  245. $(this).val('');
  246. }
  247. });
  248. $('#replymessage').focus();
  249. }
  250. /**
  251. * Send a reply in a discussion.
  252. * @param string parentid : the comment identifier we want to send a reply to.
  253. */
  254. function send_comment(parentid) {
  255. // Do not send if no data.
  256. if ($('#replymessage').val().length==0) {
  257. return;
  258. }
  259. showStatus('Sending comment...', spin=true);
  260. var cipherdata = zeroCipher(pageKey(), $('#replymessage').val());
  261. var ciphernickname = '';
  262. var nick=$('#nickname').val();
  263. if (nick != '' && nick != 'Optional nickname...') {
  264. ciphernickname = zeroCipher(pageKey(), nick);
  265. }
  266. var data_to_send = { data:cipherdata,
  267. parentid: parentid,
  268. pasteid: pasteID(),
  269. nickname: ciphernickname
  270. };
  271. $.post(scriptLocation(), data_to_send, 'json')
  272. .error(function() {
  273. showError('Comment could not be sent (server error or not responding).');
  274. })
  275. .success(function(data) {
  276. if (data.status == 0) {
  277. showStatus('Comment posted.');
  278. location.reload();
  279. }
  280. else if (data.status==1) {
  281. showError('Could not post comment: '+data.message);
  282. }
  283. else {
  284. showError('Could not post comment.');
  285. }
  286. });
  287. }
  288. /**
  289. * Send a new paste to server
  290. */
  291. function send_data() {
  292. // Do not send if no data.
  293. if ($('#message').val().length == 0) {
  294. return;
  295. }
  296. // If sjcl has not collected enough entropy yet, display a message.
  297. if (!sjcl.random.isReady())
  298. {
  299. showStatus('Sending paste (Please move your mouse for more entropy)...', spin=true);
  300. sjcl.random.addEventListener('seeded', function(){ send_data(); });
  301. return;
  302. }
  303. showStatus('Sending paste...', spin=true);
  304. var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
  305. var cipherdata = zeroCipher(randomkey, $('#message').val());
  306. var data_to_send = { data: cipherdata,
  307. expire: $('#pasteExpiration').val(),
  308. burnafterreading: $('#burnafterreading').is(':checked') ? 1 : 0,
  309. opendiscussion: $('#opendiscussion').is(':checked') ? 1 : 0
  310. };
  311. $.post(scriptLocation(), data_to_send, 'json')
  312. .error(function() {
  313. showError('Data could not be sent (serveur error or not responding).');
  314. })
  315. .success(function(data) {
  316. if (data.status == 0) {
  317. stateExistingPaste();
  318. var url = scriptLocation() + "?" + data.id + '#' + randomkey;
  319. var deleteUrl = scriptLocation() + "?pasteid=" + data.id + '&deletetoken=' + data.deletetoken;
  320. showStatus('');
  321. $('#pastelink').html('Your paste is <a id="pasteurl" href="' + url + '">' + url + '</a> <span id="copyhint">(Hit CTRL+C to copy)</span>');
  322. $('#deletelink').html('<a href="' + deleteUrl + '">Delete data</a>');
  323. $('#pasteresult').removeClass('hidden');
  324. selectText('pasteurl'); // We pre-select the link so that the user only has to CTRL+C the link.
  325. setElementText($('#cleartext'), $('#message').val());
  326. setElementText($('#prettyprint'), $('#message').val());
  327. // Convert URLs to clickable links.
  328. urls2links($('#cleartext'));
  329. urls2links($('#prettyprint'));
  330. showStatus('');
  331. if (typeof prettyPrint == 'function') prettyPrint();
  332. }
  333. else if (data.status==1) {
  334. showError('Could not create paste: '+data.message);
  335. }
  336. else {
  337. showError('Could not create paste.');
  338. }
  339. });
  340. }
  341. /** Text range selection.
  342. * From: http://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
  343. * @param string element : Indentifier of the element to select (id="").
  344. */
  345. function selectText(element) {
  346. var doc = document
  347. , text = doc.getElementById(element)
  348. , range, selection
  349. ;
  350. if (doc.body.createTextRange) { //ms
  351. range = doc.body.createTextRange();
  352. range.moveToElementText(text);
  353. range.select();
  354. } else if (window.getSelection) { //all others
  355. selection = window.getSelection();
  356. range = doc.createRange();
  357. range.selectNodeContents(text);
  358. selection.removeAllRanges();
  359. selection.addRange(range);
  360. }
  361. }
  362. /**
  363. * Put the screen in "New paste" mode.
  364. */
  365. function stateNewPaste() {
  366. $('#sendbutton').removeClass('hidden');
  367. $('#clonebutton').addClass('hidden');
  368. $('#rawtextbutton').addClass('hidden');
  369. $('#expiration').removeClass('hidden');
  370. $('#remainingtime').addClass('hidden');
  371. $('#burnafterreadingoption').removeClass('hidden');
  372. $('#opendisc').removeClass('hidden');
  373. $('#newbutton').removeClass('hidden');
  374. $('#pasteresult').addClass('hidden');
  375. $('#message').text('');
  376. $('#message').removeClass('hidden');
  377. $('#cleartext').addClass('hidden');
  378. $('#message').focus();
  379. $('#discussion').addClass('hidden');
  380. $('#prettymessage').addClass('hidden');
  381. }
  382. /**
  383. * Put the screen in "Existing paste" mode.
  384. */
  385. function stateExistingPaste() {
  386. $('#sendbutton').addClass('hidden');
  387. // No "clone" for IE<10.
  388. if ($('#oldienotice').is(":visible")) {
  389. $('#clonebutton').addClass('hidden');
  390. }
  391. else {
  392. $('#clonebutton').removeClass('hidden');
  393. }
  394. $('#rawtextbutton').removeClass('hidden');
  395. $('#expiration').addClass('hidden');
  396. $('#burnafterreadingoption').addClass('hidden');
  397. $('#opendisc').addClass('hidden');
  398. $('#newbutton').removeClass('hidden');
  399. $('#pasteresult').addClass('hidden');
  400. $('#message').addClass('hidden');
  401. $('#cleartext').addClass('hidden');
  402. $('#prettymessage').removeClass('hidden');
  403. }
  404. /** Return raw text
  405. */
  406. function rawText()
  407. {
  408. var paste = $('#cleartext').html();
  409. var newDoc = document.open('text/html', 'replace');
  410. newDoc.write('<pre>'+paste+'</pre>');
  411. newDoc.close();
  412. }
  413. /**
  414. * Clone the current paste.
  415. */
  416. function clonePaste() {
  417. stateNewPaste();
  418. //Erase the id and the key in url
  419. history.replaceState(document.title, document.title, scriptLocation());
  420. showStatus('');
  421. $('#message').text($('#cleartext').text());
  422. }
  423. /**
  424. * Create a new paste.
  425. */
  426. function newPaste() {
  427. stateNewPaste();
  428. showStatus('');
  429. $('#message').text('');
  430. }
  431. /**
  432. * Display an error message
  433. * (We use the same function for paste and reply to comments)
  434. */
  435. function showError(message) {
  436. $('#status').addClass('errorMessage').text(message);
  437. $('#replystatus').addClass('errorMessage').text(message);
  438. }
  439. /**
  440. * Display status
  441. * (We use the same function for paste and reply to comments)
  442. *
  443. * @param string message : text to display
  444. * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
  445. */
  446. function showStatus(message, spin) {
  447. $('#replystatus').removeClass('errorMessage');
  448. $('#replystatus').text(message);
  449. if (!message) {
  450. $('#status').html(' ');
  451. return;
  452. }
  453. if (message == '') {
  454. $('#status').html(' ');
  455. return;
  456. }
  457. $('#status').removeClass('errorMessage');
  458. $('#status').text(message);
  459. if (spin) {
  460. var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0px 4px 0px 0px;" />';
  461. $('#status').prepend(img);
  462. $('#replystatus').prepend(img);
  463. }
  464. }
  465. /**
  466. * Convert URLs to clickable links.
  467. * URLs to handle:
  468. * <code>
  469. * magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
  470. * http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  471. * http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  472. * </code>
  473. *
  474. * @param object element : a jQuery DOM element.
  475. * @FIXME: add ppa & apt links.
  476. */
  477. function urls2links(element) {
  478. var re = /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig;
  479. element.html(element.html().replace(re,'<a href="$1" rel="nofollow">$1</a>'));
  480. var re = /((magnet):[\w?=&.\/-;#@~%+-]+)/ig;
  481. element.html(element.html().replace(re,'<a href="$1">$1</a>'));
  482. }
  483. /**
  484. * Return the deciphering key stored in anchor part of the URL
  485. */
  486. function pageKey() {
  487. var key = window.location.hash.substring(1); // Get key
  488. // Some stupid web 2.0 services and redirectors add data AFTER the anchor
  489. // (such as &utm_source=...).
  490. // We will strip any additional data.
  491. // First, strip everything after the equal sign (=) which signals end of base64 string.
  492. i = key.indexOf('='); if (i>-1) { key = key.substring(0,i+1); }
  493. // If the equal sign was not present, some parameters may remain:
  494. i = key.indexOf('&'); if (i>-1) { key = key.substring(0,i); }
  495. // Then add trailing equal sign if it's missing
  496. if (key.charAt(key.length-1)!=='=') key+='=';
  497. return key;
  498. }
  499. $(function() {
  500. // hide "no javascript" message
  501. $('#noscript').hide();
  502. // If "burn after reading" is checked, disable discussion.
  503. $('#burnafterreading').change(function() {
  504. if ($(this).is(':checked') ) {
  505. $('#opendisc').addClass('buttondisabled');
  506. $('#opendiscussion').attr({checked: false});
  507. $('#opendiscussion').attr('disabled',true);
  508. }
  509. else {
  510. $('#opendisc').removeClass('buttondisabled');
  511. $('#opendiscussion').removeAttr('disabled');
  512. }
  513. });
  514. // Display status returned by php code if any (eg. Paste was properly deleted.)
  515. if ($('#status').text().length > 0) {
  516. showStatus($('#status').text(),false);
  517. return;
  518. }
  519. $('#status').html(' '); // Keep line height even if content empty.
  520. // Display an existing paste
  521. if ($('#cipherdata').text().length > 1) {
  522. // Missing decryption key in URL ?
  523. if (window.location.hash.length == 0) {
  524. showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL ?)');
  525. return;
  526. }
  527. // List of messages to display
  528. var messages = jQuery.parseJSON($('#cipherdata').text());
  529. // Show proper elements on screen.
  530. stateExistingPaste();
  531. displayMessages(pageKey(), messages);
  532. }
  533. // Display error message from php code.
  534. else if ($('#errormessage').text().length>1) {
  535. showError($('#errormessage').text());
  536. }
  537. // Create a new paste.
  538. else {
  539. newPaste();
  540. }
  541. });