zerobin.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  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.20
  10. */
  11. 'use strict';
  12. // Immediately start random number generator collector.
  13. sjcl.random.startCollectors();
  14. $(function() {
  15. /**
  16. * static helper methods
  17. */
  18. var helper = {
  19. /**
  20. * Converts a duration (in seconds) into human readable format.
  21. *
  22. * @param int seconds
  23. * @return string
  24. */
  25. secondsToHuman: function(seconds)
  26. {
  27. if (seconds < 60)
  28. {
  29. var v = Math.floor(seconds),
  30. format = '%d second' + ((v > 1) ? 's' : '');
  31. return i18n._(format, v);
  32. }
  33. if (seconds < 60 * 60)
  34. {
  35. var v = Math.floor(seconds / 60),
  36. format = '%d minute' + ((v > 1) ? 's' : '');
  37. return i18n._(format, v);
  38. }
  39. if (seconds < 60 * 60 * 24)
  40. {
  41. var v = Math.floor(seconds / (60 * 60)),
  42. format = '%d hour' + ((v > 1) ? 's' : '');
  43. return i18n._(format, v);
  44. }
  45. // If less than 2 months, display in days:
  46. if (seconds < 60 * 60 * 24 * 60)
  47. {
  48. var v = Math.floor(seconds / (60 * 60 * 24)),
  49. format = '%d day' + ((v > 1) ? 's' : '');
  50. return i18n._(format, v);
  51. }
  52. var v = Math.floor(seconds / (60 * 60 * 24 * 30)),
  53. format = '%d month' + ((v > 1) ? 's' : '');
  54. return i18n._(format, v);
  55. },
  56. /**
  57. * Converts an associative array to an encoded string
  58. * for appending to the anchor.
  59. *
  60. * @param object associative_array Object to be serialized
  61. * @return string
  62. */
  63. hashToParameterString: function(associativeArray)
  64. {
  65. var parameterString = '';
  66. for (key in associativeArray)
  67. {
  68. if(parameterString === '')
  69. {
  70. parameterString = encodeURIComponent(key);
  71. parameterString += '=' + encodeURIComponent(associativeArray[key]);
  72. }
  73. else
  74. {
  75. parameterString += '&' + encodeURIComponent(key);
  76. parameterString += '=' + encodeURIComponent(associativeArray[key]);
  77. }
  78. }
  79. // padding for URL shorteners
  80. parameterString += '&p=p';
  81. return parameterString;
  82. },
  83. /**
  84. * Converts a string to an associative array.
  85. *
  86. * @param string parameter_string String containing parameters
  87. * @return object
  88. */
  89. parameterStringToHash: function(parameterString)
  90. {
  91. var parameterHash = {};
  92. var parameterArray = parameterString.split('&');
  93. for (var i = 0; i < parameterArray.length; i++)
  94. {
  95. var pair = parameterArray[i].split('=');
  96. var key = decodeURIComponent(pair[0]);
  97. var value = decodeURIComponent(pair[1]);
  98. parameterHash[key] = value;
  99. }
  100. return parameterHash;
  101. },
  102. /**
  103. * Get an associative array of the parameters found in the anchor
  104. *
  105. * @return object
  106. */
  107. getParameterHash: function()
  108. {
  109. var hashIndex = window.location.href.indexOf('#');
  110. if (hashIndex >= 0)
  111. {
  112. return this.parameterStringToHash(window.location.href.substring(hashIndex + 1));
  113. }
  114. else
  115. {
  116. return {};
  117. }
  118. },
  119. /**
  120. * Convert all applicable characters to HTML entities
  121. *
  122. * @param string str
  123. * @return string encoded string
  124. */
  125. htmlEntities: function(str)
  126. {
  127. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  128. },
  129. /**
  130. * Text range selection.
  131. * From: http://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
  132. *
  133. * @param string element : Indentifier of the element to select (id="").
  134. */
  135. selectText: function(element)
  136. {
  137. var doc = document,
  138. text = doc.getElementById(element),
  139. range,
  140. selection;
  141. // MS
  142. if (doc.body.createTextRange)
  143. {
  144. range = doc.body.createTextRange();
  145. range.moveToElementText(text);
  146. range.select();
  147. }
  148. // all others
  149. else if (window.getSelection)
  150. {
  151. selection = window.getSelection();
  152. range = doc.createRange();
  153. range.selectNodeContents(text);
  154. selection.removeAllRanges();
  155. selection.addRange(range);
  156. }
  157. },
  158. /**
  159. * Set text of a DOM element (required for IE)
  160. * This is equivalent to element.text(text)
  161. *
  162. * @param object element : a DOM element.
  163. * @param string text : the text to enter.
  164. */
  165. setElementText: function(element, text)
  166. {
  167. // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
  168. if ($('#oldienotice').is(':visible')) {
  169. var html = this.htmlEntities(text).replace(/\n/ig,'\r\n<br>');
  170. element.html('<pre>'+html+'</pre>');
  171. }
  172. // for other (sane) browsers:
  173. else
  174. {
  175. element.text(text);
  176. }
  177. },
  178. /**
  179. * Convert URLs to clickable links.
  180. * URLs to handle:
  181. * <code>
  182. * magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
  183. * http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  184. * http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  185. * </code>
  186. *
  187. * @param object element : a jQuery DOM element.
  188. */
  189. urls2links: function(element)
  190. {
  191. var markup = '<a href="$1" rel="nofollow">$1</a>';
  192. element.html(
  193. element.html().replace(
  194. /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
  195. markup
  196. )
  197. );
  198. element.html(
  199. element.html().replace(
  200. /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
  201. markup
  202. )
  203. );
  204. },
  205. /**
  206. * minimal sprintf emulation for %s and %d formats
  207. * From: http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914
  208. *
  209. * @param string format
  210. * @param mixed args one or multiple parameters injected into format string
  211. * @return string
  212. */
  213. sprintf: function()
  214. {
  215. var args = arguments;
  216. if (typeof arguments[0] == 'object') args = arguments[0];
  217. var string = args[0],
  218. i = 1;
  219. return string.replace(/%((%)|s|d)/g, function (m) {
  220. // m is the matched format, e.g. %s, %d
  221. var val = null;
  222. if (m[2]) {
  223. val = m[2];
  224. } else {
  225. val = args[i];
  226. // A switch statement so that the formatter can be extended.
  227. switch (m) {
  228. case '%d':
  229. val = parseFloat(val);
  230. if (isNaN(val)) {
  231. val = 0;
  232. }
  233. break;
  234. // Default is %s
  235. }
  236. ++i;
  237. }
  238. return val;
  239. });
  240. }
  241. };
  242. /**
  243. * internationalization methods
  244. */
  245. var i18n = {
  246. supportedLanguages: ['de', 'fr', 'pl'], // and the built in 'en'
  247. /**
  248. * translate a string, alias for translate()
  249. *
  250. * @param string $messageId
  251. * @param mixed args one or multiple parameters injected into placeholders
  252. * @return string
  253. */
  254. _: function()
  255. {
  256. return this.translate(arguments);
  257. },
  258. /**
  259. * translate a string
  260. *
  261. * @param string $messageId
  262. * @param mixed args one or multiple parameters injected into placeholders
  263. * @return string
  264. */
  265. translate: function()
  266. {
  267. var args = arguments;
  268. if (typeof arguments[0] == 'object') args = arguments[0];
  269. var messageId = args[0];
  270. if (messageId.length == 0) return messageId;
  271. if (!this.translations.hasOwnProperty(messageId))
  272. {
  273. console.log('Missing translation for: ' + messageId);
  274. this.translations[messageId] = messageId;
  275. }
  276. args[0] = this.translations[messageId];
  277. return helper.sprintf(args);
  278. },
  279. loadTranslations: function(callback)
  280. {
  281. var language = (navigator.language || navigator.userLanguage).substring(0, 2);
  282. // note that 'en' is built in, so no translation is necessary
  283. if (this.supportedLanguages.indexOf(language) == -1) return;
  284. $.getJSON('i18n/' + language + '.json', function(data) {
  285. i18n.translations = data;
  286. callback();
  287. });
  288. },
  289. translations: {}
  290. }
  291. /**
  292. * filter methods
  293. */
  294. var filter = {
  295. /**
  296. * Compress a message (deflate compression). Returns base64 encoded data.
  297. *
  298. * @param string message
  299. * @return base64 string data
  300. */
  301. compress: function(message)
  302. {
  303. return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
  304. },
  305. /**
  306. * Decompress a message compressed with compress().
  307. *
  308. * @param base64 string data
  309. * @return string message
  310. */
  311. decompress: function(data)
  312. {
  313. return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
  314. },
  315. /**
  316. * Compress, then encrypt message with key.
  317. *
  318. * @param string key
  319. * @param string password
  320. * @param string message
  321. * @return encrypted string data
  322. */
  323. cipher: function(key, password, message)
  324. {
  325. password = password.trim();
  326. if (password.length == 0)
  327. {
  328. return sjcl.encrypt(key, this.compress(message));
  329. }
  330. return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.compress(message));
  331. },
  332. /**
  333. * Decrypt message with key, then decompress.
  334. *
  335. * @param string key
  336. * @param string password
  337. * @param encrypted string data
  338. * @return string readable message
  339. */
  340. decipher: function(key, password, data)
  341. {
  342. if (data != undefined)
  343. {
  344. try
  345. {
  346. return this.decompress(sjcl.decrypt(key, data));
  347. }
  348. catch(err)
  349. {
  350. try
  351. {
  352. return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
  353. }
  354. catch(err)
  355. {}
  356. }
  357. }
  358. return '';
  359. }
  360. };
  361. var zerobin = {
  362. /**
  363. * Get the current script location (without search or hash part of the URL).
  364. * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
  365. *
  366. * @return string current script location
  367. */
  368. scriptLocation: function()
  369. {
  370. var scriptLocation = window.location.href.substring(0,window.location.href.length
  371. - window.location.search.length - window.location.hash.length),
  372. hashIndex = scriptLocation.indexOf('#');
  373. if (hashIndex !== -1)
  374. {
  375. scriptLocation = scriptLocation.substring(0, hashIndex);
  376. }
  377. return scriptLocation;
  378. },
  379. /**
  380. * Get the pastes unique identifier from the URL
  381. * eg. http://server.com/zero/?c05354954c49a487#xxx --> c05354954c49a487
  382. *
  383. * @return string unique identifier
  384. */
  385. pasteID: function()
  386. {
  387. return window.location.search.substring(1);
  388. },
  389. /**
  390. * Return the deciphering key stored in anchor part of the URL
  391. *
  392. * @return string key
  393. */
  394. pageKey: function()
  395. {
  396. // Some web 2.0 services and redirectors add data AFTER the anchor
  397. // (such as &utm_source=...). We will strip any additional data.
  398. var key = window.location.hash.substring(1), // Get key
  399. i = key.indexOf('=');
  400. // First, strip everything after the equal sign (=) which signals end of base64 string.
  401. if (i > -1) key = key.substring(0, i + 1);
  402. // If the equal sign was not present, some parameters may remain:
  403. i = key.indexOf('&');
  404. if (i > -1) key = key.substring(0, i);
  405. // Then add trailing equal sign if it's missing
  406. if (key.charAt(key.length - 1) !== '=') key += '=';
  407. return key;
  408. },
  409. /**
  410. * ask the user for the password and return it
  411. *
  412. * @throws error when dialog canceled
  413. * @return string password
  414. */
  415. requestPassword: function()
  416. {
  417. var password = prompt(i18n._('Please enter the password for this paste:'), '');
  418. if (password == null) throw 'password prompt canceled';
  419. if (password.length == 0) return this.requestPassword();
  420. return password;
  421. },
  422. /**
  423. * Show decrypted text in the display area, including discussion (if open)
  424. *
  425. * @param string key : decryption key
  426. * @param array comments : Array of messages to display (items = array with keys ('data','meta')
  427. */
  428. displayMessages: function(key, comments)
  429. {
  430. // Try to decrypt the paste.
  431. var password = this.passwordInput.val();
  432. if (!this.prettyPrint.hasClass('prettyprinted')) {
  433. try
  434. {
  435. var cleartext = filter.decipher(key, password, comments[0].data);
  436. if (cleartext.length == 0)
  437. {
  438. if (password.length == 0) password = this.requestPassword();
  439. cleartext = filter.decipher(key, password, comments[0].data);
  440. }
  441. if (cleartext.length == 0) throw 'failed to decipher message';
  442. this.passwordInput.val(password);
  443. helper.setElementText(this.clearText, cleartext);
  444. helper.setElementText(this.prettyPrint, cleartext);
  445. // Convert URLs to clickable links.
  446. helper.urls2links(this.clearText);
  447. helper.urls2links(this.prettyPrint);
  448. if (typeof prettyPrint == 'function') prettyPrint();
  449. }
  450. catch(err)
  451. {
  452. this.clearText.addClass('hidden');
  453. this.prettyMessage.addClass('hidden');
  454. this.cloneButton.addClass('hidden');
  455. this.showError(i18n._('Could not decrypt data (Wrong key?)'));
  456. return;
  457. }
  458. }
  459. // Display paste expiration.
  460. if (comments[0].meta.expire_date)
  461. {
  462. this.remainingTime.removeClass('foryoureyesonly')
  463. .text(i18n._('This document will expire in %s.', helper.secondsToHuman(comments[0].meta.remaining_time)))
  464. .removeClass('hidden');
  465. }
  466. if (comments[0].meta.burnafterreading)
  467. {
  468. $.get(this.scriptLocation() + '?pasteid=' + this.pasteID() + '&deletetoken=burnafterreading', 'json')
  469. .fail(function() {
  470. zerobin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
  471. });
  472. this.remainingTime.addClass('foryoureyesonly')
  473. .text(i18n._('FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'))
  474. .removeClass('hidden');
  475. // Discourage cloning (as it can't really be prevented).
  476. this.cloneButton.addClass('hidden');
  477. }
  478. // If the discussion is opened on this paste, display it.
  479. if (comments[0].meta.opendiscussion)
  480. {
  481. this.comments.html('');
  482. // iterate over comments
  483. for (var i = 1; i < comments.length; i++)
  484. {
  485. var place = this.comments;
  486. var comment=comments[i];
  487. var cleartext = '[' + i18n._('Could not decrypt comment; Wrong key?') + ']';
  488. try
  489. {
  490. cleartext = filter.decipher(key, password, comment.data);
  491. }
  492. catch(err)
  493. {}
  494. // If parent comment exists, display below (CSS will automatically shift it right.)
  495. var cname = '#comment_' + comment.meta.parentid;
  496. // If the element exists in page
  497. if ($(cname).length)
  498. {
  499. place = $(cname);
  500. }
  501. var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
  502. + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
  503. + '<button class="btn btn-default btn-sm">' + i18n._('Reply') + '</button>'
  504. + '</div></article>');
  505. divComment.find('button').click({commentid: comment.meta.commentid}, $.proxy(this.openReply, this));
  506. helper.setElementText(divComment.find('div.commentdata'), cleartext);
  507. // Convert URLs to clickable links in comment.
  508. helper.urls2links(divComment.find('div.commentdata'));
  509. // Try to get optional nickname:
  510. var nick = filter.decipher(key, password, comment.meta.nickname);
  511. if (nick.length > 0)
  512. {
  513. divComment.find('span.nickname').text(nick);
  514. }
  515. else
  516. {
  517. divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
  518. }
  519. divComment.find('span.commentdate')
  520. .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
  521. .attr('title', 'CommentID: ' + comment.meta.commentid);
  522. // If an avatar is available, display it.
  523. if (comment.meta.vizhash)
  524. {
  525. divComment.find('span.nickname')
  526. .before(
  527. '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
  528. i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
  529. );
  530. }
  531. place.append(divComment);
  532. }
  533. var divComment = $(
  534. '<div class="comment"><button class="btn btn-default btn-sm">' +
  535. i18n._('Add comment') + '</button></div>'
  536. );
  537. divComment.find('button').click({commentid: this.pasteID()}, $.proxy(this.openReply, this));
  538. this.comments.append(divComment);
  539. this.discussion.removeClass('hidden');
  540. }
  541. },
  542. /**
  543. * Open the comment entry when clicking the "Reply" button of a comment.
  544. *
  545. * @param Event event
  546. */
  547. openReply: function(event)
  548. {
  549. event.preventDefault();
  550. var source = $(event.target),
  551. commentid = event.data.commentid
  552. hint = i18n._('Optional nickname...');
  553. // Remove any other reply area.
  554. $('div.reply').remove();
  555. var reply = $(
  556. '<div class="reply">' +
  557. '<input type="text" id="nickname" class="form-control" title="' + hint + '" placeholder="' + hint + '" />' +
  558. '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>' +
  559. '<br /><button id="replybutton" class="btn btn-default btn-sm">' + i18n._('Post comment') + '</button>' +
  560. '<div id="replystatus"> </div>' +
  561. '</div>'
  562. );
  563. reply.find('button').click({parentid: commentid}, $.proxy(this.sendComment, this));
  564. source.after(reply);
  565. $('#replymessage').focus();
  566. },
  567. /**
  568. * Send a reply in a discussion.
  569. *
  570. * @param Event event
  571. */
  572. sendComment: function(event)
  573. {
  574. event.preventDefault();
  575. this.errorMessage.addClass('hidden');
  576. // Do not send if no data.
  577. var replyMessage = $('#replymessage');
  578. if (replyMessage.val().length == 0) return;
  579. this.showStatus(i18n._('Sending comment...'), true);
  580. var parentid = event.data.parentid;
  581. var cipherdata = filter.cipher(this.pageKey(), this.passwordInput.val(), replyMessage.val());
  582. var ciphernickname = '';
  583. var nick = $('#nickname').val();
  584. if (nick != '')
  585. {
  586. ciphernickname = filter.cipher(this.pageKey(), this.passwordInput.val(), nick);
  587. }
  588. var data_to_send = {
  589. data: cipherdata,
  590. parentid: parentid,
  591. pasteid: this.pasteID(),
  592. nickname: ciphernickname
  593. };
  594. $.post(this.scriptLocation(), data_to_send, function(data)
  595. {
  596. if (data.status == 0)
  597. {
  598. zerobin.showStatus(i18n._('Comment posted.'), false);
  599. $.get(zerobin.scriptLocation() + '?' + zerobin.pasteID() + '&json', function(data)
  600. {
  601. if (data.status == 0)
  602. {
  603. zerobin.displayMessages(zerobin.pageKey(), data.messages);
  604. }
  605. else if (data.status == 1)
  606. {
  607. zerobin.showError(i18n._('Could not refresh display: %s', data.message));
  608. }
  609. else
  610. {
  611. zerobin.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
  612. }
  613. }, 'json')
  614. .fail(function() {
  615. zerobin.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
  616. });
  617. }
  618. else if (data.status == 1)
  619. {
  620. zerobin.showError(i18n._('Could not post comment: %s', data.message));
  621. }
  622. else
  623. {
  624. zerobin.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
  625. }
  626. }, 'json')
  627. .fail(function() {
  628. zerobin.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
  629. });
  630. },
  631. /**
  632. * Send a new paste to server
  633. *
  634. * @param Event event
  635. */
  636. sendData: function(event)
  637. {
  638. event.preventDefault();
  639. // Do not send if no data.
  640. if (this.message.val().length == 0) return;
  641. // If sjcl has not collected enough entropy yet, display a message.
  642. if (!sjcl.random.isReady())
  643. {
  644. this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
  645. sjcl.random.addEventListener('seeded', function() {
  646. this.sendData(event);
  647. });
  648. return;
  649. }
  650. this.showStatus(i18n._('Sending paste...'), true);
  651. var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
  652. var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val());
  653. var data_to_send = {
  654. data: cipherdata,
  655. expire: $('#pasteExpiration').val(),
  656. burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
  657. opendiscussion: this.openDiscussion.is(':checked') ? 1 : 0
  658. };
  659. $.post(this.scriptLocation(), data_to_send, function(data)
  660. {
  661. if (data.status == 0) {
  662. zerobin.stateExistingPaste();
  663. var url = zerobin.scriptLocation() + '?' + data.id + '#' + randomkey;
  664. var deleteUrl = zerobin.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
  665. zerobin.showStatus('', false);
  666. zerobin.errorMessage.addClass('hidden');
  667. $('#pastelink').html(i18n._('Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>', url, url));
  668. $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
  669. zerobin.pasteResult.removeClass('hidden');
  670. // We pre-select the link so that the user only has to [Ctrl]+[c] the link.
  671. helper.selectText('pasteurl');
  672. helper.setElementText(zerobin.clearText, zerobin.message.val());
  673. helper.setElementText(zerobin.prettyPrint, zerobin.message.val());
  674. // Convert URLs to clickable links.
  675. helper.urls2links(zerobin.clearText);
  676. helper.urls2links(zerobin.prettyPrint);
  677. zerobin.showStatus('', false);
  678. if (typeof prettyPrint == 'function') prettyPrint();
  679. }
  680. else if (data.status==1)
  681. {
  682. zerobin.showError(i18n._('Could not create paste: %s', data.message));
  683. }
  684. else
  685. {
  686. zerobin.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
  687. }
  688. }, 'json')
  689. .fail(function() {
  690. zerobin.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
  691. });
  692. },
  693. /**
  694. * Put the screen in "New paste" mode.
  695. */
  696. stateNewPaste: function()
  697. {
  698. this.message.text('');
  699. this.cloneButton.addClass('hidden');
  700. this.rawTextButton.addClass('hidden');
  701. this.remainingTime.addClass('hidden');
  702. this.pasteResult.addClass('hidden');
  703. this.clearText.addClass('hidden');
  704. this.discussion.addClass('hidden');
  705. this.prettyMessage.addClass('hidden');
  706. this.sendButton.removeClass('hidden');
  707. this.expiration.removeClass('hidden');
  708. this.burnAfterReadingOption.removeClass('hidden');
  709. this.openDisc.removeClass('hidden');
  710. this.newButton.removeClass('hidden');
  711. this.password.removeClass('hidden');
  712. this.message.removeClass('hidden');
  713. this.message.focus();
  714. },
  715. /**
  716. * Put the screen in "Existing paste" mode.
  717. */
  718. stateExistingPaste: function()
  719. {
  720. this.sendButton.addClass('hidden');
  721. // No "clone" for IE<10.
  722. if ($('#oldienotice').is(":visible"))
  723. {
  724. this.cloneButton.addClass('hidden');
  725. }
  726. else
  727. {
  728. this.cloneButton.removeClass('hidden');
  729. }
  730. this.rawTextButton.removeClass('hidden');
  731. this.expiration.addClass('hidden');
  732. this.burnAfterReadingOption.addClass('hidden');
  733. this.openDisc.addClass('hidden');
  734. this.newButton.removeClass('hidden');
  735. this.pasteResult.addClass('hidden');
  736. this.message.addClass('hidden');
  737. this.clearText.addClass('hidden');
  738. this.prettyMessage.removeClass('hidden');
  739. },
  740. /**
  741. * If "burn after reading" is checked, disable discussion.
  742. */
  743. changeBurnAfterReading: function()
  744. {
  745. if (this.burnAfterReading.is(':checked') )
  746. {
  747. this.openDisc.addClass('buttondisabled');
  748. this.openDiscussion.attr({checked: false, disabled: true});
  749. }
  750. else
  751. {
  752. this.openDisc.removeClass('buttondisabled');
  753. this.openDiscussion.removeAttr('disabled');
  754. }
  755. },
  756. /**
  757. * Reload the page
  758. *
  759. * @param Event event
  760. */
  761. reloadPage: function(event)
  762. {
  763. event.preventDefault();
  764. window.location.href = this.scriptLocation();
  765. },
  766. /**
  767. * Return raw text
  768. *
  769. * @param Event event
  770. */
  771. rawText: function(event)
  772. {
  773. event.preventDefault();
  774. var paste = this.clearText.html();
  775. var newDoc = document.open('text/html', 'replace');
  776. newDoc.write('<pre>' + paste + '</pre>');
  777. newDoc.close();
  778. },
  779. /**
  780. * Clone the current paste.
  781. *
  782. * @param Event event
  783. */
  784. clonePaste: function(event)
  785. {
  786. event.preventDefault();
  787. this.stateNewPaste();
  788. // Erase the id and the key in url
  789. history.replaceState(document.title, document.title, this.scriptLocation());
  790. this.showStatus('', false);
  791. this.message.text(this.clearText.text());
  792. },
  793. /**
  794. * Create a new paste.
  795. */
  796. newPaste: function()
  797. {
  798. this.stateNewPaste();
  799. this.showStatus('', false);
  800. this.message.text('');
  801. },
  802. /**
  803. * Display an error message
  804. * (We use the same function for paste and reply to comments)
  805. *
  806. * @param string message : text to display
  807. */
  808. showError: function(message)
  809. {
  810. if (this.status.length)
  811. {
  812. this.status.addClass('errorMessage').text(message);
  813. }
  814. else
  815. {
  816. this.errorMessage.removeClass('hidden');
  817. var content = this.errorMessage.contents();
  818. content[content.length - 1].nodeValue = ' ' + message;
  819. }
  820. this.replyStatus.addClass('errorMessage').text(message);
  821. },
  822. /**
  823. * Display a status message
  824. * (We use the same function for paste and reply to comments)
  825. *
  826. * @param string message : text to display
  827. * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
  828. */
  829. showStatus: function(message, spin)
  830. {
  831. this.replyStatus.removeClass('errorMessage').text(message);
  832. if (!message)
  833. {
  834. this.status.html(' ');
  835. return;
  836. }
  837. if (message == '')
  838. {
  839. this.status.html(' ');
  840. return;
  841. }
  842. this.status.removeClass('errorMessage').text(message);
  843. if (spin)
  844. {
  845. var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
  846. this.status.prepend(img);
  847. this.replyStatus.prepend(img);
  848. }
  849. },
  850. /**
  851. * bind events to DOM elements
  852. */
  853. bindEvents: function()
  854. {
  855. this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
  856. this.sendButton.click($.proxy(this.sendData, this));
  857. this.cloneButton.click($.proxy(this.clonePaste, this));
  858. this.rawTextButton.click($.proxy(this.rawText, this));
  859. $('.reloadlink').click($.proxy(this.reloadPage, this));
  860. },
  861. /**
  862. * main application
  863. */
  864. init: function()
  865. {
  866. // hide "no javascript" message
  867. $('#noscript').hide();
  868. // preload jQuery wrapped DOM elements and bind events
  869. this.burnAfterReading = $('#burnafterreading');
  870. this.burnAfterReadingOption = $('#burnafterreadingoption');
  871. this.cipherData = $('#cipherdata');
  872. this.clearText = $('#cleartext');
  873. this.cloneButton = $('#clonebutton');
  874. this.comments = $('#comments');
  875. this.discussion = $('#discussion');
  876. this.errorMessage = $('#errormessage');
  877. this.expiration = $('#expiration');
  878. this.message = $('#message');
  879. this.newButton = $('#newbutton');
  880. this.openDisc = $('#opendisc');
  881. this.openDiscussion = $('#opendiscussion');
  882. this.password = $('#password');
  883. this.passwordInput = $('#passwordinput');
  884. this.pasteResult = $('#pasteresult');
  885. this.prettyMessage = $('#prettymessage');
  886. this.prettyPrint = $('#prettyprint');
  887. this.rawTextButton = $('#rawtextbutton');
  888. this.remainingTime = $('#remainingtime');
  889. this.replyStatus = $('#replystatus');
  890. this.sendButton = $('#sendbutton');
  891. this.status = $('#status');
  892. this.bindEvents();
  893. // Display status returned by php code if any (eg. Paste was properly deleted.)
  894. if (this.status.text().length > 0)
  895. {
  896. this.showStatus(this.status.text(), false);
  897. return;
  898. }
  899. // Keep line height even if content empty.
  900. this.status.html(' ');
  901. // Display an existing paste
  902. if (this.cipherData.text().length > 1)
  903. {
  904. // Missing decryption key in URL?
  905. if (window.location.hash.length == 0)
  906. {
  907. this.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)'));
  908. return;
  909. }
  910. // List of messages to display.
  911. var messages = $.parseJSON(this.cipherData.text());
  912. // Show proper elements on screen.
  913. this.stateExistingPaste();
  914. this.displayMessages(this.pageKey(), messages);
  915. }
  916. // Display error message from php code.
  917. else if (this.errorMessage.text().length > 1)
  918. {
  919. this.showError(this.errorMessage.text());
  920. }
  921. // Create a new paste.
  922. else
  923. {
  924. this.newPaste();
  925. }
  926. }
  927. }
  928. /**
  929. * main application start, called when DOM is fully loaded
  930. * runs zerobin when translations were loaded
  931. */
  932. i18n.loadTranslations($.proxy(zerobin.init, zerobin));
  933. });