zerobin.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /* ZeroBin 0.15 - http://sebsauvage.net/wiki/doku.php?id=php:zerobin */
  2. // Immediately start random number generator collector.
  3. sjcl.random.startCollectors();
  4. // Converts a duration (in seconds) into human readable format.
  5. function secondsToHuman(seconds)
  6. {
  7. if (seconds<60) { var v=Math.floor(seconds); return v+' second'+((v>1)?'s':''); }
  8. if (seconds<60*60) { var v=Math.floor(seconds/60); return v+' minute'+((v>1)?'s':''); }
  9. if (seconds<60*60*24) { var v=Math.floor(seconds/(60*60)); return v+' hour'+((v>1)?'s':''); }
  10. // If less than 2 months, display in days:
  11. if (seconds<60*60*24*60) { var v=Math.floor(seconds/(60*60*24)); return v+' day'+((v>1)?'s':''); }
  12. var v=Math.floor(seconds/(60*60*24*30)); return v+' month'+((v>1)?'s':'');
  13. }
  14. // Compress a message (deflate compression). Returns base64 encoded data.
  15. function compress(message) { return Base64.toBase64(RawDeflate.deflate(Base64.utob(message))); }
  16. // Decompress a message compressed with compress().
  17. function decompress(data) { return Base64.btou(RawDeflate.inflate(Base64.fromBase64(data))) }
  18. // Compress, then encrypt message with key.
  19. function zeroCipher(key,message)
  20. {
  21. return sjcl.encrypt(key,compress(message));
  22. }
  23. // Decrypt message with key, then decompress.
  24. function zeroDecipher(key,data)
  25. {
  26. return decompress(sjcl.decrypt(key,data));
  27. }
  28. // Returns the current script location (without search or hash part of the URL).
  29. // eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
  30. function scriptLocation()
  31. {
  32. return window.location.href.substring(0,window.location.href.length
  33. -window.location.search.length -window.location.hash.length);
  34. }
  35. // Returns the paste unique identifier from the URL
  36. // eg. 'c05354954c49a487'
  37. function pasteID()
  38. {
  39. return window.location.search.substring(1);
  40. }
  41. // Set text of a DOM element (required for IE)
  42. // This is equivalent to element.text(text)
  43. // Input: element : a DOM element.
  44. // text : the text to enter.
  45. function setElementText(element,text)
  46. {
  47. if ($('div#oldienotice').is(":visible")) // For IE<10.
  48. {
  49. // IE<10 do not support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
  50. element.text(text.replace(/\n/ig,'{BIG_UGLY_STINKING_THING__OH_GOD_I_HATE_IE}'));
  51. element.html(element.text().replace(/{BIG_UGLY_STINKING_THING__OH_GOD_I_HATE_IE}/ig,"\r\n<br>"));
  52. }
  53. else // for other (sane) browsers:
  54. {
  55. element.text(text);
  56. }
  57. }
  58. // Show decrypted text in the display area, including discussion (if open)
  59. // Input: messages (Array) : Array of messages to display (items = array with keys ('data','meta')
  60. // key (string): decryption key
  61. function displayMessages(key,comments)
  62. {
  63. try { // Try to decrypt the paste.
  64. var cleartext = zeroDecipher(key,comments[0].data);
  65. } catch(err) {
  66. $('div#cleartext').hide();
  67. $('button#clonebutton').hide();
  68. showError('Could not decrypt data (Wrong key ?)');
  69. return;
  70. }
  71. setElementText($('div#cleartext'),cleartext);
  72. urls2links($('div#cleartext')); // Convert URLs to clickable links.
  73. // Display paste expiration.
  74. if (comments[0].meta.expire_date) $('div#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').show();
  75. if (comments[0].meta.burnafterreading)
  76. {
  77. $('div#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.').show();
  78. $('button#clonebutton').hide(); // Discourage cloning (as it can't really be prevented).
  79. }
  80. // If the discussion is opened on this paste, display it.
  81. if (comments[0].meta.opendiscussion)
  82. {
  83. $('div#comments').html('');
  84. for (var i = 1; i < comments.length; i++) // For each comment.
  85. {
  86. var comment=comments[i];
  87. var cleartext="[Could not decrypt comment ; Wrong key ?]";
  88. try { cleartext = zeroDecipher(key,comment.data); } catch(err) { }
  89. var place = $('div#comments');
  90. // If parent comment exists, display below (CSS will automatically shift it right.)
  91. var cname = 'div#comment_'+comment.meta.parentid
  92. if ($(cname).length) place = $(cname); // If the element exists in page
  93. var divComment = $('<div class="comment" id="comment_'+comment.meta.commentid+'">'
  94. +'<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
  95. +'<button onclick="open_reply($(this),\''+comment.meta.commentid+'\');return false;">Reply</button>'
  96. +'</div>');
  97. setElementText(divComment.find('div.commentdata'),cleartext);
  98. urls2links(divComment.find('div.commentdata')); // Convert URLs to clickable links in comment.
  99. divComment.find('span.nickname').html('<i>(Anonymous)</i>');
  100. // Try to get optional nickname:
  101. try { divComment.find('span.nickname').text(zeroDecipher(key,comment.meta.nickname));} catch(err) { }
  102. divComment.find('span.commentdate').text(' ('+(new Date(comment.meta.postdate*1000).toUTCString())+')').attr('title','CommentID: '+comment.meta.commentid);
  103. // If an avatar is available, display it.
  104. if (comment.meta.vizhash)
  105. divComment.find('span.nickname').before('<img src="'+comment.meta.vizhash+'" class="vizhash" title="Anonymous avatar (Vizhash of the IP address)" />');
  106. place.append(divComment);
  107. }
  108. $('div#comments').append('<div class="comment"><button onclick="open_reply($(this),\''+pasteID()+'\');return false;">Add comment</button></div>');
  109. $('div#discussion').show();
  110. }
  111. }
  112. // Open the comment entry when clicking the "Reply" button of a comment.
  113. // source = element which emitted the event.
  114. // commentid = identifier of the comment we want to reply to.
  115. function open_reply(source,commentid)
  116. {
  117. $('div.reply').remove(); // Remove any other reply area.
  118. source.after('<div class="reply">'
  119. +'<input type="text" id="nickname" title="Optional nickname..." value="Optional nickname..." />'
  120. +'<textarea id="replymessage" class="replymessage" cols="80" rows="7"></textarea>'
  121. +'<br><button id="replybutton" onclick="send_comment(\''+commentid+'\');return false;">Post comment</button>'
  122. +'<div id="replystatus">&nbsp;</div>'
  123. +'</div>');
  124. $('input#nickname').focus(function() {
  125. $(this).css('color', '#000');
  126. if($(this).val()==$(this).attr('title')) $(this).val('');
  127. });
  128. $('textarea#replymessage').focus();
  129. }
  130. // Send a reply in a discussion.
  131. // Input: parentid : the comment identifier we want to send a reply to.
  132. function send_comment(parentid)
  133. {
  134. if ($('textarea#replymessage').val().length==0) return; // Do not send if no data.
  135. showStatus('Sending comment...',spin=true);
  136. var cipherdata=zeroCipher(pageKey(), $('textarea#replymessage').val());
  137. var ciphernickname='';
  138. var nick=$('input#nickname').val();
  139. if (nick!='' && nick!='Optional nickname...') ciphernickname=ciphernickname=zeroCipher(pageKey(),nick);
  140. var data_to_send = { data:cipherdata,
  141. parentid: parentid,
  142. pasteid: pasteID(),
  143. nickname: ciphernickname
  144. };
  145. $.post(scriptLocation(), data_to_send ,'json' )
  146. .error( function() { showError('Comment could not be sent (serveur error or not responding).'); } )
  147. .success(function(data)
  148. {
  149. if (data.status==0)
  150. {
  151. showStatus('Comment posted.');
  152. location.reload();
  153. }
  154. else if (data.status==1)
  155. {
  156. showError('Could not post comment: '+data.message);
  157. }
  158. else
  159. {
  160. showError('Could not post comment.');
  161. }
  162. }
  163. );
  164. }
  165. // Send a new paste to server
  166. function send_data()
  167. {
  168. if ($('textarea#message').val().length==0) return; // Do not send if no data.
  169. showStatus('Sending paste...',spin=true);
  170. var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8,0),0);
  171. var cipherdata = zeroCipher(randomkey,$('textarea#message').val());
  172. var data_to_send = { data:cipherdata,
  173. expire:$('select#pasteExpiration').val(),
  174. opendiscussion:$('input#opendiscussion').is(':checked')?1:0
  175. };
  176. $.post(scriptLocation(), data_to_send ,'json' )
  177. .error( function() { showError('Data could not be sent (serveur error or not responding).'); } )
  178. .success(function(data)
  179. {
  180. if (data.status==0)
  181. {
  182. stateExistingPaste();
  183. var url=scriptLocation()+"?"+data.id+'#'+randomkey;
  184. showStatus('');
  185. $('div#pastelink').html('Your paste is <a href="'+url+'">'+url+'</a>');
  186. $('div#pastelink').append('&nbsp;&nbsp;<button id="shortenbutton" onclick="document.location=\''+shortenUrl(url)+'\'"><img src="lib/icon_shorten.png" width="13" height="15" />Shorten URL</button>').show();
  187. setElementText($('div#cleartext'),$('textarea#message').val());
  188. urls2links($('div#cleartext'));
  189. showStatus('');
  190. }
  191. else if (data.status==1)
  192. {
  193. showError('Could not create paste: '+data.message);
  194. }
  195. else
  196. {
  197. showError('Could not create paste.');
  198. }
  199. }
  200. );
  201. }
  202. // Put the screen in "New paste" mode.
  203. function stateNewPaste()
  204. {
  205. $('button#sendbutton').show();
  206. $('button#clonebutton').hide();
  207. $('div#expiration').show();
  208. $('div#remainingtime').hide();
  209. $('div#language').hide(); // $('#language').show();
  210. $('input#password').hide(); //$('#password').show();
  211. $('div#opendisc').show();
  212. $('button#newbutton').show();
  213. $('div#pastelink').hide();
  214. $('textarea#message').text('');
  215. $('textarea#message').show();
  216. $('div#cleartext').hide();
  217. $('div#message').focus();
  218. $('div#discussion').hide();
  219. }
  220. // Put the screen in "Existing paste" mode.
  221. function stateExistingPaste()
  222. {
  223. $('button#sendbutton').hide();
  224. if (!$('div#oldienotice').is(":visible")) $('button#clonebutton').show(); // No "clone" for IE<10.
  225. $('button#clonebutton').show();// FIXME
  226. $('div#expiration').hide();
  227. $('div#language').hide();
  228. $('input#password').hide();
  229. $('div#opendisc').hide();
  230. $('button#newbutton').show();
  231. $('div#pastelink').hide();
  232. $('textarea#message').hide();
  233. $('div#cleartext').show();
  234. }
  235. // Clone the current paste.
  236. function clonePaste()
  237. {
  238. stateNewPaste();
  239. showStatus('');
  240. $('textarea#message').text($('div#cleartext').text());
  241. }
  242. // Create a new paste.
  243. function newPaste()
  244. {
  245. stateNewPaste();
  246. showStatus('');
  247. $('textarea#message').text('');
  248. }
  249. // Display an error message
  250. // (We use the same function for paste and reply to comments)
  251. function showError(message)
  252. {
  253. $('div#status').addClass('errorMessage').text(message);
  254. $('div#replystatus').addClass('errorMessage').text(message);
  255. }
  256. // Display status
  257. // (We use the same function for paste and reply to comments)
  258. // message (string) = text to display
  259. // spin (boolean, optional) = tell if the "spinning" animation should be displayed.
  260. function showStatus(message,spin)
  261. {
  262. $('div#replystatus').removeClass('errorMessage');
  263. $('div#replystatus').text(message);
  264. if (!message) { $('div#status').html('&nbsp'); return; }
  265. if (message=='') { $('div#status').html('&nbsp'); return; }
  266. $('div#status').removeClass('errorMessage');
  267. $('div#status').text(message);
  268. if (spin)
  269. {
  270. var img = '<img src="lib/busy.gif" style="width:16px;height:9px;margin:0px 4px 0px 0px;" />';
  271. $('div#status').prepend(img);
  272. $('div#replystatus').prepend(img);
  273. }
  274. }
  275. // Generate link to URL shortener.
  276. function shortenUrl(url)
  277. {
  278. return 'http://snipurl.com/site/snip?link='+encodeURIComponent(url);
  279. }
  280. // Convert URLs to clickable links.
  281. // Input: element : a jQuery DOM element.
  282. // Example URLs to handle:
  283. // magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
  284. // http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  285. // http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  286. // FIXME: add ppa & apt links.
  287. function urls2links(element)
  288. {
  289. var re = /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig;
  290. element.html(element.html().replace(re,'<a href="$1" rel="nofollow">$1</a>'));
  291. var re = /((magnet):[\w?=&.\/-;#@~%+-]+)/ig;
  292. element.html(element.html().replace(re,'<a href="$1">$1</a>'));
  293. }
  294. // Return the deciphering key stored in anchor part of the URL
  295. function pageKey()
  296. {
  297. var key = window.location.hash.substring(1); // Get key
  298. // Some stupid web 2.0 services and redirectors add data AFTER the anchor
  299. // (such as &utm_source=...).
  300. // We will strip any additional data.
  301. // First, strip everything after the equal sign (=) which signals end of base64 string.
  302. i = key.indexOf('='); if (i>-1) { key = key.substring(0,i+1); }
  303. // If the equal sign was not present, some parameters may remain:
  304. i = key.indexOf('&'); if (i>-1) { key = key.substring(0,i); }
  305. // Then add trailing equal sign if it's missing
  306. if (key.charAt(key.length-1)!=='=') key+='=';
  307. return key;
  308. }
  309. $(document).ready(function() {
  310. $('select#pasteExpiration').change(function() {
  311. if ($(this).val()=='burn') { $('div#opendisc').addClass('buttondisabled'); $('input#opendiscussion').attr('disabled',true); }
  312. else { $('div#opendisc').removeClass('buttondisabled'); $('input#opendiscussion').removeAttr('disabled'); }
  313. });
  314. if ($('div#cipherdata').text().length>1) // Display an existing paste
  315. {
  316. if (window.location.hash.length==0) // Missing decryption key in URL ?
  317. {
  318. showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL ?)');
  319. return;
  320. }
  321. var messages = jQuery.parseJSON($('div#cipherdata').text()); // List of messages to display
  322. stateExistingPaste(); // Show proper elements on screen.
  323. displayMessages(pageKey(),messages);
  324. }
  325. else if ($('div#errormessage').text().length>1) // Display error message from php code.
  326. {
  327. showError($('div#errormessage').text());
  328. }
  329. else // Create a new paste.
  330. {
  331. newPaste();
  332. }
  333. });