privatebin.js 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968
  1. /**
  2. * PrivateBin
  3. *
  4. * a zero-knowledge paste bin
  5. *
  6. * @see {@link https://github.com/PrivateBin/PrivateBin}
  7. * @copyright 2012 Sébastien SAUVAGE ({@link http://sebsauvage.net})
  8. * @license {@link https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
  9. * @version 1.1
  10. * @name PrivateBin
  11. * @namespace
  12. */
  13. 'use strict';
  14. /** global: Base64 */
  15. /** global: FileReader */
  16. /** global: RawDeflate */
  17. /** global: history */
  18. /** global: navigator */
  19. /** global: prettyPrint */
  20. /** global: prettyPrintOne */
  21. /** global: showdown */
  22. /** global: sjcl */
  23. // Immediately start random number generator collector.
  24. sjcl.random.startCollectors();
  25. // jQuery(document).ready(function() {
  26. // // startup
  27. // }
  28. jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
  29. /**
  30. * static helper methods
  31. *
  32. * @param {object} window
  33. * @param {object} document
  34. * @name helper
  35. * @class
  36. */
  37. var helper = (function (window, document) {
  38. var me = {};
  39. /**
  40. * character to HTML entity lookup table
  41. *
  42. * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
  43. * @private
  44. * @enum {Object}
  45. * @readonly
  46. */
  47. var entityMap = {
  48. '&': '&',
  49. '<': '&lt;',
  50. '>': '&gt;',
  51. '"': '&quot;',
  52. "'": '&#39;',
  53. '/': '&#x2F;',
  54. '`': '&#x60;',
  55. '=': '&#x3D;'
  56. };
  57. /**
  58. * cache for script location
  59. *
  60. * @private
  61. * @enum {string|null}
  62. */
  63. var scriptLocation = null;
  64. /**
  65. * converts a duration (in seconds) into human friendly approximation
  66. *
  67. * @name helper.secondsToHuman
  68. * @function
  69. * @param {number} seconds
  70. * @return {Array}
  71. */
  72. me.secondsToHuman = function(seconds)
  73. {
  74. var v;
  75. if (seconds < 60)
  76. {
  77. v = Math.floor(seconds);
  78. return [v, 'second'];
  79. }
  80. if (seconds < 60 * 60)
  81. {
  82. v = Math.floor(seconds / 60);
  83. return [v, 'minute'];
  84. }
  85. if (seconds < 60 * 60 * 24)
  86. {
  87. v = Math.floor(seconds / (60 * 60));
  88. return [v, 'hour'];
  89. }
  90. // If less than 2 months, display in days:
  91. if (seconds < 60 * 60 * 24 * 60)
  92. {
  93. v = Math.floor(seconds / (60 * 60 * 24));
  94. return [v, 'day'];
  95. }
  96. v = Math.floor(seconds / (60 * 60 * 24 * 30));
  97. return [v, 'month'];
  98. };
  99. /**
  100. * text range selection
  101. *
  102. * @see {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
  103. * @name helper.selectText
  104. * @function
  105. * @param {HTMLElement} element
  106. */
  107. me.selectText = function(element)
  108. {
  109. var range, selection;
  110. // MS
  111. if (document.body.createTextRange)
  112. {
  113. range = document.body.createTextRange();
  114. range.moveToElementText(element);
  115. range.select();
  116. }
  117. // all others
  118. else if (window.getSelection)
  119. {
  120. selection = window.getSelection();
  121. range = document.createRange();
  122. range.selectNodeContents(element);
  123. selection.removeAllRanges();
  124. selection.addRange(range);
  125. }
  126. };
  127. /**
  128. * set text of a DOM element (required for IE),
  129. *
  130. * @name helper.setElementText
  131. * @function
  132. * @param {Object} element - a DOM element
  133. * @param {string} text - the text to enter
  134. * @this is equivalent to element.text(text)
  135. * @TODO check for XSS attacks, usually no CSS can prevent them so this looks weird on the first look
  136. */
  137. me.setElementText = function(element, text)
  138. {
  139. // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
  140. if ($('#oldienotice').is(':visible')) {
  141. var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
  142. element.html('<pre>' + html + '</pre>');
  143. }
  144. // for other (sane) browsers:
  145. else
  146. {
  147. element.text(text);
  148. }
  149. };
  150. /**
  151. * replace last child of element with message
  152. *
  153. * @name helper.setMessage
  154. * @function
  155. * @param {Object} element - a jQuery wrapped DOM element
  156. * @param {string} message - the message to append
  157. */
  158. me.setMessage = function(element, message)
  159. {
  160. var content = element.contents();
  161. if (content.length > 0)
  162. {
  163. content[content.length - 1].nodeValue = ' ' + message;
  164. }
  165. else
  166. {
  167. me.setElementText(element, message);
  168. }
  169. };
  170. /**
  171. * convert URLs to clickable links.
  172. * URLs to handle:
  173. * <pre>
  174. * magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
  175. * http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  176. * http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  177. * </pre>
  178. *
  179. * @name helper.urls2links
  180. * @function
  181. * @param {Object} element - a jQuery DOM element
  182. */
  183. me.urls2links = function(element)
  184. {
  185. var markup = '<a href="$1" rel="nofollow">$1</a>';
  186. element.html(
  187. element.html().replace(
  188. /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
  189. markup
  190. )
  191. );
  192. element.html(
  193. element.html().replace(
  194. /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
  195. markup
  196. )
  197. );
  198. };
  199. /**
  200. * minimal sprintf emulation for %s and %d formats
  201. *
  202. * @see {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
  203. * @name helper.sprintf
  204. * @function
  205. * @param {string} format
  206. * @param {...*} args - one or multiple parameters injected into format string
  207. * @return {string}
  208. */
  209. me.sprintf = function()
  210. {
  211. var args = arguments;
  212. if (typeof arguments[0] === 'object')
  213. {
  214. args = arguments[0];
  215. }
  216. var format = args[0],
  217. i = 1;
  218. return format.replace(/%((%)|s|d)/g, function (m) {
  219. // m is the matched format, e.g. %s, %d
  220. var val;
  221. if (m[2]) {
  222. val = m[2];
  223. } else {
  224. val = args[i];
  225. // A switch statement so that the formatter can be extended.
  226. switch (m)
  227. {
  228. case '%d':
  229. val = parseFloat(val);
  230. if (isNaN(val)) {
  231. val = 0;
  232. }
  233. break;
  234. default:
  235. // Default is %s
  236. }
  237. ++i;
  238. }
  239. return val;
  240. });
  241. };
  242. /**
  243. * get value of cookie, if it was set, empty string otherwise
  244. *
  245. * @see {@link http://www.w3schools.com/js/js_cookies.asp}
  246. * @name helper.getCookie
  247. * @function
  248. * @param {string} cname
  249. * @return {string}
  250. */
  251. me.getCookie = function(cname) {
  252. var name = cname + '=',
  253. ca = document.cookie.split(';');
  254. for (var i = 0; i < ca.length; ++i) {
  255. var c = ca[i];
  256. while (c.charAt(0) === ' ')
  257. {
  258. c = c.substring(1);
  259. }
  260. if (c.indexOf(name) === 0)
  261. {
  262. return c.substring(name.length, c.length);
  263. }
  264. }
  265. return '';
  266. };
  267. /**
  268. * get the current script location (without search or hash part of the URL),
  269. * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
  270. *
  271. * @name helper.scriptLocation
  272. * @function
  273. * @return {string} current script location
  274. */
  275. me.scriptLocation = function()
  276. {
  277. // check for cached version
  278. if (scriptLocation !== null) {
  279. return scriptLocation;
  280. }
  281. scriptLocation = window.location.href.substring(
  282. 0,
  283. window.location.href.length - window.location.search.length - window.location.hash.length
  284. );
  285. var hashIndex = scriptLocation.indexOf('?');
  286. if (hashIndex !== -1)
  287. {
  288. scriptLocation = scriptLocation.substring(0, hashIndex);
  289. }
  290. return scriptLocation;
  291. };
  292. /**
  293. * get the pastes unique identifier from the URL,
  294. * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
  295. *
  296. * @name helper.pasteId
  297. * @function
  298. * @return {string} unique identifier
  299. */
  300. me.pasteId = function()
  301. {
  302. return window.location.search.substring(1);
  303. };
  304. /**
  305. * return the deciphering key stored in anchor part of the URL
  306. *
  307. * @name helper.pageKey
  308. * @function
  309. * @return {string} key
  310. */
  311. me.pageKey = function()
  312. {
  313. var key = window.location.hash.substring(1),
  314. i = key.indexOf('&');
  315. // Some web 2.0 services and redirectors add data AFTER the anchor
  316. // (such as &utm_source=...). We will strip any additional data.
  317. if (i > -1)
  318. {
  319. key = key.substring(0, i);
  320. }
  321. return key;
  322. };
  323. /**
  324. * convert all applicable characters to HTML entities
  325. *
  326. * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
  327. * @name helper.htmlEntities
  328. * @function
  329. * @param {string} str
  330. * @return {string} escaped HTML
  331. */
  332. me.htmlEntities = function(str) {
  333. return String(str).replace(
  334. /[&<>"'`=\/]/g, function(s) {
  335. return entityMap[s];
  336. });
  337. };
  338. return me;
  339. })(window, document);
  340. /**
  341. * internationalization methods
  342. *
  343. * @param {object} window
  344. * @param {object} document
  345. * @name i18n
  346. * @class
  347. */
  348. var i18n = (function (window, document) {
  349. var me = {};
  350. /**
  351. * supported languages, minus the built in 'en'
  352. *
  353. * @private
  354. * @prop {string[]}
  355. * @readonly
  356. */
  357. var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh'];
  358. /**
  359. * built in language
  360. *
  361. * @private
  362. * @prop {string}
  363. */
  364. var language = 'en';
  365. /**
  366. * translation cache
  367. *
  368. * @private
  369. * @enum {Object}
  370. */
  371. var translations = {};
  372. /**
  373. * translate a string, alias for i18n.translate()
  374. *
  375. * @name i18n._
  376. * @function
  377. * @param {string} messageId
  378. * @param {...*} args - one or multiple parameters injected into placeholders
  379. * @return {string}
  380. */
  381. me._ = function()
  382. {
  383. return me.translate(arguments);
  384. };
  385. /**
  386. * translate a string
  387. *
  388. * @name i18n.translate
  389. * @function
  390. * @param {string} messageId
  391. * @param {...*} args - one or multiple parameters injected into placeholders
  392. * @return {string}
  393. */
  394. me.translate = function()
  395. {
  396. var args = arguments, messageId;
  397. if (typeof arguments[0] === 'object')
  398. {
  399. args = arguments[0];
  400. }
  401. var usesPlurals = $.isArray(args[0]);
  402. if (usesPlurals)
  403. {
  404. // use the first plural form as messageId, otherwise the singular
  405. messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
  406. }
  407. else
  408. {
  409. messageId = args[0];
  410. }
  411. if (messageId.length === 0)
  412. {
  413. return messageId;
  414. }
  415. if (!translations.hasOwnProperty(messageId))
  416. {
  417. if (language !== 'en')
  418. {
  419. console.error(
  420. 'Missing ' + language + ' translation for: ' + messageId
  421. );
  422. }
  423. translations[messageId] = args[0];
  424. }
  425. if (usesPlurals && $.isArray(translations[messageId]))
  426. {
  427. var n = parseInt(args[1] || 1, 10),
  428. key = me.getPluralForm(n),
  429. maxKey = translations[messageId].length - 1;
  430. if (key > maxKey)
  431. {
  432. key = maxKey;
  433. }
  434. args[0] = translations[messageId][key];
  435. args[1] = n;
  436. }
  437. else
  438. {
  439. args[0] = translations[messageId];
  440. }
  441. return helper.sprintf(args);
  442. };
  443. /**
  444. * per language functions to use to determine the plural form
  445. *
  446. * @see {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
  447. * @name i18n.getPluralForm
  448. * @function
  449. * @param {number} n
  450. * @return {number} array key
  451. */
  452. me.getPluralForm = function(n) {
  453. switch (language)
  454. {
  455. case 'fr':
  456. case 'oc':
  457. case 'zh':
  458. return (n > 1 ? 1 : 0);
  459. case 'pl':
  460. return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
  461. case 'ru':
  462. return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
  463. case 'sl':
  464. return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
  465. // de, en, es, it, no
  466. default:
  467. return (n !== 1 ? 1 : 0);
  468. }
  469. };
  470. /**
  471. * load translations into cache, then trigger controller initialization
  472. *
  473. * @name i18n.loadTranslations
  474. * @function
  475. */
  476. me.loadTranslations = function()
  477. {
  478. var newLanguage = helper.getCookie('lang');
  479. // auto-select language based on browser settings
  480. if (newLanguage.length === 0)
  481. {
  482. newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
  483. }
  484. // if language is already used (e.g, default 'en'), skip update
  485. if (newLanguage === language)
  486. {
  487. controller.init();
  488. return;
  489. }
  490. // if language is not supported, show error
  491. if (supportedLanguages.indexOf(newLanguage) === -1)
  492. {
  493. console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
  494. controller.init();
  495. }
  496. // load strongs from JSON
  497. $.getJSON('i18n/' + newLanguage + '.json', function(data) {
  498. language = newLanguage;
  499. translations = data;
  500. }).fail(function (data, textStatus, errorMsg) {
  501. console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
  502. });
  503. controller.init();
  504. };
  505. return me;
  506. })(window, document);
  507. /**
  508. * filter methods
  509. *
  510. * @param {object} window
  511. * @param {object} document
  512. * @name filter
  513. * @class
  514. */
  515. var filter = (function (window, document) {
  516. var me = {};
  517. /**
  518. * compress a message (deflate compression), returns base64 encoded data
  519. *
  520. * @name filter.compress
  521. * @function
  522. * @param {string} message
  523. * @return {string} base64 data
  524. */
  525. me.compress = function(message)
  526. {
  527. return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
  528. },
  529. /**
  530. * decompress a message compressed with filter.compress()
  531. *
  532. * @name filter.decompress
  533. * @function
  534. * @param {string} data - base64 data
  535. * @return {string} message
  536. */
  537. me.decompress = function(data)
  538. {
  539. return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
  540. },
  541. /**
  542. * compress, then encrypt message with given key and password
  543. *
  544. * @name filter.cipher
  545. * @function
  546. * @param {string} key
  547. * @param {string} password
  548. * @param {string} message
  549. * @return {string} data - JSON with encrypted data
  550. */
  551. me.cipher = function(key, password, message)
  552. {
  553. // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
  554. var options = {mode: 'gcm', ks: 256, ts: 128};
  555. if ((password || '').trim().length === 0)
  556. {
  557. return sjcl.encrypt(key, me.compress(message), options);
  558. }
  559. return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), me.compress(message), options);
  560. },
  561. /**
  562. * decrypt message with key, then decompress
  563. *
  564. * @name filter.decipher
  565. * @function
  566. * @param {string} key
  567. * @param {string} password
  568. * @param {string} data - JSON with encrypted data
  569. * @return {string} decrypted message
  570. */
  571. me.decipher = function(key, password, data)
  572. {
  573. if (data !== undefined)
  574. {
  575. try
  576. {
  577. return me.decompress(sjcl.decrypt(key, data));
  578. }
  579. catch(err)
  580. {
  581. try
  582. {
  583. return me.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
  584. }
  585. catch(e)
  586. {
  587. // ignore error, because ????? @TODO
  588. }
  589. }
  590. }
  591. return '';
  592. }
  593. return me;
  594. })(window, document);
  595. /**
  596. * PrivateBin logic
  597. *
  598. * @param {object} window
  599. * @param {object} document
  600. * @name controller
  601. * @class
  602. */
  603. var controller = (function (window, document) {
  604. var me = {};
  605. /**
  606. * headers to send in AJAX requests
  607. *
  608. * @private
  609. * @enum {Object}
  610. */
  611. var headers = {'X-Requested-With': 'JSONHttpRequest'};
  612. /**
  613. * URL shortners create address
  614. *
  615. * @private
  616. * @prop {string}
  617. */
  618. var shortenerUrl = '';
  619. /**
  620. * URL of newly created paste
  621. *
  622. * @private
  623. * @prop {string}
  624. */
  625. var createdPasteUrl = '';
  626. // jQuery pre-loaded objects
  627. var $attach,
  628. $attachment,
  629. $attachmentLink,
  630. $burnAfterReading,
  631. $burnAfterReadingOption,
  632. $cipherData,
  633. $clearText,
  634. $cloneButton,
  635. $clonedFile,
  636. $comments,
  637. $discussion,
  638. $errorMessage,
  639. $expiration,
  640. $fileRemoveButton,
  641. $fileWrap,
  642. $formatter,
  643. $image,
  644. $loadingIndicator,
  645. $message,
  646. $messageEdit,
  647. $messagePreview,
  648. $newButton,
  649. $openDisc, // @TODO: rename - too similar to openDiscussion, difference unclear
  650. $openDiscussion,
  651. $password,
  652. $passwordInput,
  653. $passwordModal,
  654. $passwordForm,
  655. $passwordDecrypt,
  656. $pasteResult,
  657. $pasteUrl,
  658. $prettyMessage,
  659. $prettyPrint,
  660. $preview,
  661. $rawTextButton,
  662. $remainingTime,
  663. $replyStatus,
  664. $sendButton,
  665. $status;
  666. /**
  667. * ask the user for the password and set it
  668. *
  669. * @name controller.requestPassword
  670. * @function
  671. */
  672. me.requestPassword = function()
  673. {
  674. if ($passwordModal.length === 0) {
  675. var password = prompt(i18n._('Please enter the password for this paste:'), '');
  676. if (password === null)
  677. {
  678. throw 'password prompt canceled';
  679. }
  680. if (password.length === 0)
  681. {
  682. // recursive…
  683. me.requestPassword();
  684. } else {
  685. $passwordInput.val(password);
  686. me.displayMessages();
  687. }
  688. } else {
  689. $passwordModal.modal();
  690. }
  691. };
  692. /**
  693. * use given format on paste, defaults to plain text
  694. *
  695. * @name controller.formatPaste
  696. * @function
  697. * @param {string} format
  698. * @param {string} text
  699. */
  700. me.formatPaste = function(format, text)
  701. {
  702. helper.setElementText($clearText, text);
  703. helper.setElementText($prettyPrint, text);
  704. switch (format || 'plaintext') {
  705. case 'markdown':
  706. // silently fail if showdown is not available
  707. // @TODO: maybe better show an error message? At least a warning?
  708. if (typeof showdown === 'object')
  709. {
  710. var converter = new showdown.Converter({
  711. strikethrough: true,
  712. tables: true,
  713. tablesHeaderId: true
  714. });
  715. $clearText.html(
  716. converter.makeHtml(text)
  717. );
  718. // add table classes from bootstrap css
  719. $clearText.find('table').addClass('table-condensed table-bordered');
  720. $clearText.removeClass('hidden');
  721. } else {
  722. console.error('showdown is not loaded, could not parse Markdown');
  723. }
  724. $prettyMessage.addClass('hidden');
  725. break;
  726. case 'syntaxhighlighting':
  727. // silently fail if prettyprint is not available
  728. // @TODO: maybe better show an error message? At least a warning?
  729. if (typeof prettyPrintOne === 'function')
  730. {
  731. if (typeof prettyPrint === 'function')
  732. {
  733. prettyPrint();
  734. }
  735. $prettyPrint.html(
  736. prettyPrintOne(
  737. helper.htmlEntities(text), null, true
  738. )
  739. );
  740. } else {
  741. console.error('pretty print is not loaded, could not link ');
  742. }
  743. // fall through, as the rest is the same
  744. default: // = 'plaintext'
  745. // convert URLs to clickable links
  746. helper.urls2links($clearText);
  747. helper.urls2links($prettyPrint);
  748. $clearText.addClass('hidden');
  749. $prettyPrint.css('white-space', 'pre-wrap');
  750. $prettyPrint.css('word-break', 'normal');
  751. $prettyPrint.removeClass('prettyprint');
  752. $prettyMessage.removeClass('hidden');
  753. }
  754. };
  755. /**
  756. * show decrypted text in the display area, including discussion (if open)
  757. *
  758. * @name controller.displayMessages
  759. * @function
  760. * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
  761. */
  762. me.displayMessages = function(paste)
  763. {
  764. paste = paste || $.parseJSON($cipherData.text());
  765. var key = helper.pageKey(),
  766. password = $passwordInput.val();
  767. if (!$prettyPrint.hasClass('prettyprinted')) {
  768. // Try to decrypt the paste.
  769. try
  770. {
  771. if (paste.attachment)
  772. {
  773. var attachment = filter.decipher(key, password, paste.attachment);
  774. if (attachment.length === 0)
  775. {
  776. if (password.length === 0)
  777. {
  778. me.requestPassword();
  779. return;
  780. }
  781. attachment = filter.decipher(key, password, paste.attachment);
  782. }
  783. if (attachment.length === 0)
  784. {
  785. throw 'failed to decipher attachment';
  786. }
  787. if (paste.attachmentname)
  788. {
  789. var attachmentname = filter.decipher(key, password, paste.attachmentname);
  790. if (attachmentname.length > 0)
  791. {
  792. $attachmentLink.attr('download', attachmentname);
  793. }
  794. }
  795. $attachmentLink.attr('href', attachment);
  796. $attachment.removeClass('hidden');
  797. // if the attachment is an image, display it
  798. var imagePrefix = 'data:image/';
  799. if (attachment.substring(0, imagePrefix.length) === imagePrefix)
  800. {
  801. $image.html(
  802. $(document.createElement('img'))
  803. .attr('src', attachment)
  804. .attr('class', 'img-thumbnail')
  805. );
  806. $image.removeClass('hidden');
  807. }
  808. }
  809. var cleartext = filter.decipher(key, password, paste.data);
  810. if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
  811. {
  812. me.requestPassword();
  813. return;
  814. }
  815. if (cleartext.length === 0 && !paste.attachment)
  816. {
  817. throw 'failed to decipher message';
  818. }
  819. $passwordInput.val(password);
  820. if (cleartext.length > 0)
  821. {
  822. $('#pasteFormatter').val(paste.meta.formatter);
  823. me.formatPaste(paste.meta.formatter, cleartext);
  824. }
  825. }
  826. catch(err)
  827. {
  828. me.stateOnlyNewPaste();
  829. me.showError(i18n._('Could not decrypt data (Wrong key?)'));
  830. return;
  831. }
  832. }
  833. // display paste expiration / for your eyes only
  834. if (paste.meta.expire_date)
  835. {
  836. var expiration = helper.secondsToHuman(paste.meta.remaining_time),
  837. expirationLabel = [
  838. 'This document will expire in %d ' + expiration[1] + '.',
  839. 'This document will expire in %d ' + expiration[1] + 's.'
  840. ];
  841. helper.setMessage($remainingTime, i18n._(expirationLabel, expiration[0]));
  842. $remainingTime.removeClass('foryoureyesonly')
  843. .removeClass('hidden');
  844. }
  845. if (paste.meta.burnafterreading)
  846. {
  847. // unfortunately many web servers don't support DELETE (and PUT) out of the box
  848. $.ajax({
  849. type: 'POST',
  850. url: helper.scriptLocation() + '?' + helper.pasteId(),
  851. data: {deletetoken: 'burnafterreading'},
  852. dataType: 'json',
  853. headers: headers
  854. })
  855. .fail(function() {
  856. controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
  857. });
  858. helper.setMessage($remainingTime, i18n._(
  859. 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
  860. ));
  861. $remainingTime.addClass('foryoureyesonly')
  862. .removeClass('hidden');
  863. // discourage cloning (as it can't really be prevented)
  864. $cloneButton.addClass('hidden');
  865. }
  866. // if the discussion is opened on this paste, display it
  867. if (paste.meta.opendiscussion)
  868. {
  869. $comments.html('');
  870. // iterate over comments
  871. for (var i = 0; i < paste.comments.length; ++i)
  872. {
  873. var $place = $comments,
  874. comment = paste.comments[i],
  875. commenttext = filter.decipher(key, password, comment.data),
  876. // if parent comment exists, display below (CSS will automatically shift it to the right)
  877. cname = '#comment_' + comment.parentid,
  878. divComment = $('<article><div class="comment" id="comment_' + comment.id
  879. + '"><div class="commentmeta"><span class="nickname"></span>'
  880. + '<span class="commentdate"></span></div>'
  881. + '<div class="commentdata"></div>'
  882. + '<button class="btn btn-default btn-sm">'
  883. + i18n._('Reply') + '</button></div></article>'),
  884. divCommentData = divComment.find('div.commentdata');
  885. // if the element exists in page
  886. if ($(cname).length)
  887. {
  888. $place = $(cname);
  889. }
  890. divComment.find('button').click({commentid: comment.id}, me.openReply);
  891. helper.setElementText(divCommentData, commenttext);
  892. helper.urls2links(divCommentData);
  893. // try to get optional nickname
  894. var nick = filter.decipher(key, password, comment.meta.nickname);
  895. if (nick.length > 0)
  896. {
  897. divComment.find('span.nickname').text(nick);
  898. }
  899. else
  900. {
  901. divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
  902. }
  903. divComment.find('span.commentdate')
  904. .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
  905. .attr('title', 'CommentID: ' + comment.id);
  906. // if an avatar is available, display it
  907. if (comment.meta.vizhash)
  908. {
  909. divComment.find('span.nickname')
  910. .before(
  911. '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
  912. i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
  913. );
  914. }
  915. $place.append(divComment);
  916. }
  917. var divComment = $(
  918. '<div class="comment"><button class="btn btn-default btn-sm">' +
  919. i18n._('Add comment') + '</button></div>'
  920. );
  921. divComment.find('button').click({commentid: helper.pasteId()}, me.openReply);
  922. $comments.append(divComment);
  923. $discussion.removeClass('hidden');
  924. }
  925. };
  926. /**
  927. * open the comment entry when clicking the "Reply" button of a comment
  928. *
  929. * @name controller.openReply
  930. * @function
  931. * @param {Event} event
  932. */
  933. me.openReply = function(event)
  934. {
  935. event.preventDefault();
  936. // remove any other reply area
  937. $('div.reply').remove();
  938. var source = $(event.target),
  939. commentid = event.data.commentid,
  940. hint = i18n._('Optional nickname...'),
  941. reply = $(
  942. '<div class="reply"><input type="text" id="nickname" ' +
  943. 'class="form-control" title="' + hint + '" placeholder="' +
  944. hint + '" /><textarea id="replymessage" class="replymessage ' +
  945. 'form-control" cols="80" rows="7"></textarea><br />' +
  946. '<div id="replystatus"></div><button id="replybutton" ' +
  947. 'class="btn btn-default btn-sm">' + i18n._('Post comment') +
  948. '</button></div>'
  949. );
  950. reply.find('button').click(
  951. {parentid: commentid},
  952. me.sendComment
  953. );
  954. source.after(reply);
  955. $replyStatus = $('#replystatus');
  956. $('#replymessage').focus();
  957. };
  958. /**
  959. * send a reply in a discussion
  960. *
  961. * @name controller.sendComment
  962. * @function
  963. * @param {Event} event
  964. */
  965. me.sendComment = function(event)
  966. {
  967. event.preventDefault();
  968. $errorMessage.addClass('hidden');
  969. // do not send if no data
  970. var replyMessage = $('#replymessage');
  971. if (replyMessage.val().length === 0)
  972. {
  973. return;
  974. }
  975. me.showStatus(i18n._('Sending comment...'), true);
  976. var parentid = event.data.parentid,
  977. key = helper.pageKey(),
  978. cipherdata = filter.cipher(key, $passwordInput.val(), replyMessage.val()),
  979. ciphernickname = '',
  980. nick = $('#nickname').val();
  981. if (nick.length > 0)
  982. {
  983. ciphernickname = filter.cipher(key, $passwordInput.val(), nick);
  984. }
  985. var data_to_send = {
  986. data: cipherdata,
  987. parentid: parentid,
  988. pasteid: helper.pasteId(),
  989. nickname: ciphernickname
  990. };
  991. $.ajax({
  992. type: 'POST',
  993. url: helper.scriptLocation(),
  994. data: data_to_send,
  995. dataType: 'json',
  996. headers: headers,
  997. success: function(data)
  998. {
  999. if (data.status === 0)
  1000. {
  1001. controller.showStatus(i18n._('Comment posted.'));
  1002. $.ajax({
  1003. type: 'GET',
  1004. url: helper.scriptLocation() + '?' + helper.pasteId(),
  1005. dataType: 'json',
  1006. headers: headers,
  1007. success: function(data)
  1008. {
  1009. if (data.status === 0)
  1010. {
  1011. controller.displayMessages(data);
  1012. }
  1013. else if (data.status === 1)
  1014. {
  1015. controller.showError(i18n._('Could not refresh display: %s', data.message));
  1016. }
  1017. else
  1018. {
  1019. controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
  1020. }
  1021. }
  1022. })
  1023. .fail(function() {
  1024. controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
  1025. });
  1026. }
  1027. else if (data.status === 1)
  1028. {
  1029. controller.showError(i18n._('Could not post comment: %s', data.message));
  1030. }
  1031. else
  1032. {
  1033. controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
  1034. }
  1035. }
  1036. })
  1037. .fail(function() {
  1038. controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
  1039. });
  1040. };
  1041. /**
  1042. * send a new paste to server
  1043. *
  1044. * @name controller.sendData
  1045. * @function
  1046. * @param {Event} event
  1047. */
  1048. me.sendData = function(event)
  1049. {
  1050. event.preventDefault();
  1051. var file = document.getElementById('file'),
  1052. files = (file && file.files) ? file.files : null; // FileList object
  1053. // do not send if no data.
  1054. if ($message.val().length === 0 && !(files && files[0]))
  1055. {
  1056. return;
  1057. }
  1058. // if sjcl has not collected enough entropy yet, display a message
  1059. if (!sjcl.random.isReady())
  1060. {
  1061. me.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
  1062. sjcl.random.addEventListener('seeded', function() {
  1063. me.sendData(event);
  1064. });
  1065. return;
  1066. }
  1067. $('.navbar-toggle').click();
  1068. $password.addClass('hidden');
  1069. me.showStatus(i18n._('Sending paste...'), true);
  1070. me.stateSubmittingPaste();
  1071. var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0),
  1072. password = $passwordInput.val();
  1073. if(files && files[0])
  1074. {
  1075. if(typeof FileReader === undefined)
  1076. {
  1077. // revert loading status…
  1078. me.stateNewPaste();
  1079. me.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
  1080. return;
  1081. }
  1082. var reader = new FileReader();
  1083. // closure to capture the file information
  1084. reader.onload = (function(theFile)
  1085. {
  1086. return function(e) {
  1087. controller.sendDataContinue(
  1088. randomkey,
  1089. filter.cipher(randomkey, password, e.target.result),
  1090. filter.cipher(randomkey, password, theFile.name)
  1091. );
  1092. };
  1093. })(files[0]);
  1094. reader.readAsDataURL(files[0]);
  1095. }
  1096. else if($attachmentLink.attr('href'))
  1097. {
  1098. me.sendDataContinue(
  1099. randomkey,
  1100. filter.cipher(randomkey, password, $attachmentLink.attr('href')),
  1101. $attachmentLink.attr('download')
  1102. );
  1103. }
  1104. else
  1105. {
  1106. me.sendDataContinue(randomkey, '', '');
  1107. }
  1108. };
  1109. /**
  1110. * send a new paste to server, step 2
  1111. *
  1112. * @name controller.sendDataContinue
  1113. * @function
  1114. * @param {string} randomkey
  1115. * @param {string} cipherdata_attachment
  1116. * @param {string} cipherdata_attachment_name
  1117. */
  1118. me.sendDataContinue = function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
  1119. {
  1120. var cipherdata = filter.cipher(randomkey, $passwordInput.val(), $message.val()),
  1121. data_to_send = {
  1122. data: cipherdata,
  1123. expire: $('#pasteExpiration').val(),
  1124. formatter: $('#pasteFormatter').val(),
  1125. burnafterreading: $burnAfterReading.is(':checked') ? 1 : 0,
  1126. opendiscussion: $openDiscussion.is(':checked') ? 1 : 0
  1127. };
  1128. if (cipherdata_attachment.length > 0)
  1129. {
  1130. data_to_send.attachment = cipherdata_attachment;
  1131. if (cipherdata_attachment_name.length > 0)
  1132. {
  1133. data_to_send.attachmentname = cipherdata_attachment_name;
  1134. }
  1135. }
  1136. $.ajax({
  1137. type: 'POST',
  1138. url: helper.scriptLocation(),
  1139. data: data_to_send,
  1140. dataType: 'json',
  1141. headers: headers,
  1142. success: function(data)
  1143. {
  1144. if (data.status === 0) {
  1145. me.stateExistingPaste();
  1146. var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey,
  1147. deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
  1148. me.showStatus('');
  1149. $errorMessage.addClass('hidden');
  1150. // show new URL in browser bar
  1151. history.pushState({type: 'newpaste'}, document.title, url);
  1152. $('#pastelink').html(
  1153. i18n._(
  1154. 'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
  1155. url, url
  1156. ) + me.shortenUrl(url)
  1157. );
  1158. // save newly created element
  1159. $pasteUrl = $('#pasteurl');
  1160. // and add click event
  1161. $pasteUrl.click(me.pasteLinkClick);
  1162. var shortenButton = $('#shortenbutton');
  1163. if (shortenButton) {
  1164. shortenButton.click(me.sendToShortener);
  1165. }
  1166. $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
  1167. $pasteResult.removeClass('hidden');
  1168. // we pre-select the link so that the user only has to [Ctrl]+[c] the link
  1169. helper.selectText($pasteUrl[0]);
  1170. me.showStatus('');
  1171. me.formatPaste(data_to_send.formatter, $message.val());
  1172. }
  1173. else if (data.status === 1)
  1174. {
  1175. // revert loading status…
  1176. controller.stateNewPaste();
  1177. controller.showError(i18n._('Could not create paste: %s', data.message));
  1178. }
  1179. else
  1180. {
  1181. // revert loading status…
  1182. controller.stateNewPaste();
  1183. controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
  1184. }
  1185. }
  1186. })
  1187. .fail(function()
  1188. {
  1189. // revert loading status…
  1190. me.stateNewPaste();
  1191. controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
  1192. });
  1193. };
  1194. /**
  1195. * check if a URL shortener was defined and create HTML containing a link to it
  1196. *
  1197. * @name controller.shortenUrl
  1198. * @function
  1199. * @param {string} url
  1200. * @return {string} html
  1201. */
  1202. me.shortenUrl = function(url)
  1203. {
  1204. var shortenerHtml = $('#shortenbutton');
  1205. if (shortenerHtml) {
  1206. shortenerUrl = shortenerHtml.data('shortener');
  1207. createdPasteUrl = url;
  1208. return ' ' + $('<div />').append(shortenerHtml.clone()).html();
  1209. }
  1210. return '';
  1211. };
  1212. /**
  1213. * put the screen in "New paste" mode
  1214. *
  1215. * @name controller.stateNewPaste
  1216. * @function
  1217. */
  1218. me.stateNewPaste = function()
  1219. {
  1220. $message.text('');
  1221. $attachment.addClass('hidden');
  1222. $cloneButton.addClass('hidden');
  1223. $rawTextButton.addClass('hidden');
  1224. $remainingTime.addClass('hidden');
  1225. $pasteResult.addClass('hidden');
  1226. $clearText.addClass('hidden');
  1227. $discussion.addClass('hidden');
  1228. $prettyMessage.addClass('hidden');
  1229. $loadingIndicator.addClass('hidden');
  1230. $sendButton.removeClass('hidden');
  1231. $expiration.removeClass('hidden');
  1232. $formatter.removeClass('hidden');
  1233. $burnAfterReadingOption.removeClass('hidden');
  1234. $openDisc.removeClass('hidden');
  1235. $newButton.removeClass('hidden');
  1236. $password.removeClass('hidden');
  1237. $attach.removeClass('hidden');
  1238. $message.removeClass('hidden');
  1239. $preview.removeClass('hidden');
  1240. $message.focus();
  1241. };
  1242. /**
  1243. * put the screen in mode after submitting a paste
  1244. *
  1245. * @name controller.stateSubmittingPaste
  1246. * @function
  1247. */
  1248. me.stateSubmittingPaste = function()
  1249. {
  1250. $message.text('');
  1251. $attachment.addClass('hidden');
  1252. $cloneButton.addClass('hidden');
  1253. $rawTextButton.addClass('hidden');
  1254. $remainingTime.addClass('hidden');
  1255. $pasteResult.addClass('hidden');
  1256. $clearText.addClass('hidden');
  1257. $discussion.addClass('hidden');
  1258. $prettyMessage.addClass('hidden');
  1259. $sendButton.addClass('hidden');
  1260. $expiration.addClass('hidden');
  1261. $formatter.addClass('hidden');
  1262. $burnAfterReadingOption.addClass('hidden');
  1263. $openDisc.addClass('hidden');
  1264. $newButton.addClass('hidden');
  1265. $password.addClass('hidden');
  1266. $attach.addClass('hidden');
  1267. $message.addClass('hidden');
  1268. $preview.addClass('hidden');
  1269. $loadingIndicator.removeClass('hidden');
  1270. };
  1271. /**
  1272. * put the screen in a state where the only option is to submit a
  1273. * new paste
  1274. *
  1275. * @name controller.stateOnlyNewPaste
  1276. * @function
  1277. */
  1278. me.stateOnlyNewPaste = function()
  1279. {
  1280. $message.text('');
  1281. $attachment.addClass('hidden');
  1282. $cloneButton.addClass('hidden');
  1283. $rawTextButton.addClass('hidden');
  1284. $remainingTime.addClass('hidden');
  1285. $pasteResult.addClass('hidden');
  1286. $clearText.addClass('hidden');
  1287. $discussion.addClass('hidden');
  1288. $prettyMessage.addClass('hidden');
  1289. $sendButton.addClass('hidden');
  1290. $expiration.addClass('hidden');
  1291. $formatter.addClass('hidden');
  1292. $burnAfterReadingOption.addClass('hidden');
  1293. $openDisc.addClass('hidden');
  1294. $password.addClass('hidden');
  1295. $attach.addClass('hidden');
  1296. $message.addClass('hidden');
  1297. $preview.addClass('hidden');
  1298. $loadingIndicator.addClass('hidden');
  1299. $newButton.removeClass('hidden');
  1300. };
  1301. /**
  1302. * put the screen in "Existing paste" mode
  1303. *
  1304. * @name controller.stateExistingPaste
  1305. * @function
  1306. * @param {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false
  1307. */
  1308. me.stateExistingPaste = function(preview)
  1309. {
  1310. preview = preview || false;
  1311. if (!preview)
  1312. {
  1313. // no "clone" for IE<10.
  1314. if ($('#oldienotice').is(":visible"))
  1315. {
  1316. $cloneButton.addClass('hidden');
  1317. }
  1318. else
  1319. {
  1320. $cloneButton.removeClass('hidden');
  1321. }
  1322. $rawTextButton.removeClass('hidden');
  1323. $sendButton.addClass('hidden');
  1324. $attach.addClass('hidden');
  1325. $expiration.addClass('hidden');
  1326. $formatter.addClass('hidden');
  1327. $burnAfterReadingOption.addClass('hidden');
  1328. $openDisc.addClass('hidden');
  1329. $newButton.removeClass('hidden');
  1330. $preview.addClass('hidden');
  1331. }
  1332. $pasteResult.addClass('hidden');
  1333. $message.addClass('hidden');
  1334. $clearText.addClass('hidden');
  1335. $prettyMessage.addClass('hidden');
  1336. $loadingIndicator.addClass('hidden');
  1337. };
  1338. /**
  1339. * when "burn after reading" is checked, disable discussion
  1340. *
  1341. * @name controller.changeBurnAfterReading
  1342. * @function
  1343. */
  1344. me.changeBurnAfterReading = function()
  1345. {
  1346. if ($burnAfterReading.is(':checked') )
  1347. {
  1348. $openDisc.addClass('buttondisabled');
  1349. $openDiscussion.attr({checked: false, disabled: true});
  1350. }
  1351. else
  1352. {
  1353. $openDisc.removeClass('buttondisabled');
  1354. $openDiscussion.removeAttr('disabled');
  1355. }
  1356. };
  1357. /**
  1358. * when discussion is checked, disable "burn after reading"
  1359. *
  1360. * @name controller.changeOpenDisc
  1361. * @function
  1362. */
  1363. me.changeOpenDisc = function()
  1364. {
  1365. if ($openDiscussion.is(':checked') )
  1366. {
  1367. $burnAfterReadingOption.addClass('buttondisabled');
  1368. $burnAfterReading.attr({checked: false, disabled: true});
  1369. }
  1370. else
  1371. {
  1372. $burnAfterReadingOption.removeClass('buttondisabled');
  1373. $burnAfterReading.removeAttr('disabled');
  1374. }
  1375. };
  1376. /**
  1377. * forward to URL shortener
  1378. *
  1379. * @name controller.sendToShortener
  1380. * @function
  1381. * @param {Event} event
  1382. */
  1383. me.sendToShortener = function(event)
  1384. {
  1385. window.location.href = shortenerUrl + encodeURIComponent(createdPasteUrl);
  1386. event.preventDefault();
  1387. };
  1388. /**
  1389. * reload the page
  1390. *
  1391. * This takes the user to the PrivateBin home page.
  1392. *
  1393. * @name controller.reloadPage
  1394. * @function
  1395. * @param {Event} event
  1396. */
  1397. me.reloadPage = function(event)
  1398. {
  1399. window.location.href = helper.scriptLocation();
  1400. event.preventDefault();
  1401. };
  1402. /**
  1403. * return raw text
  1404. *
  1405. * @name controller.rawText
  1406. * @function
  1407. * @param {Event} event
  1408. */
  1409. me.rawText = function(event)
  1410. {
  1411. var paste = $('#pasteFormatter').val() === 'markdown' ?
  1412. $prettyPrint.text() : $clearText.text();
  1413. history.pushState(
  1414. null, document.title, helper.scriptLocation() + '?' +
  1415. helper.pasteId() + '#' + helper.pageKey()
  1416. );
  1417. // we use text/html instead of text/plain to avoid a bug when
  1418. // reloading the raw text view (it reverts to type text/html)
  1419. var newDoc = document.open('text/html', 'replace');
  1420. newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
  1421. newDoc.close();
  1422. event.preventDefault();
  1423. };
  1424. /**
  1425. * clone the current paste
  1426. *
  1427. * @name controller.clonePaste
  1428. * @function
  1429. * @param {Event} event
  1430. */
  1431. me.clonePaste = function(event)
  1432. {
  1433. event.preventDefault();
  1434. me.stateNewPaste();
  1435. // erase the id and the key in url
  1436. history.replaceState(null, document.title, helper.scriptLocation());
  1437. me.showStatus('');
  1438. if ($attachmentLink.attr('href'))
  1439. {
  1440. $clonedFile.removeClass('hidden');
  1441. $fileWrap.addClass('hidden');
  1442. }
  1443. $message.text(
  1444. $('#pasteFormatter').val() === 'markdown' ?
  1445. $prettyPrint.text() : $clearText.text()
  1446. );
  1447. $('.navbar-toggle').click();
  1448. };
  1449. /**
  1450. * set the expiration on bootstrap templates
  1451. *
  1452. * @name controller.setExpiration
  1453. * @function
  1454. * @param {Event} event
  1455. */
  1456. me.setExpiration = function(event)
  1457. {
  1458. event.preventDefault();
  1459. var target = $(event.target);
  1460. $('#pasteExpiration').val(target.data('expiration'));
  1461. $('#pasteExpirationDisplay').text(target.text());
  1462. };
  1463. /**
  1464. * set the format on bootstrap templates
  1465. *
  1466. * @name controller.setFormat
  1467. * @function
  1468. * @param {Event} event
  1469. */
  1470. me.setFormat = function(event)
  1471. {
  1472. var target = $(event.target);
  1473. $('#pasteFormatter').val(target.data('format'));
  1474. $('#pasteFormatterDisplay').text(target.text());
  1475. if ($messagePreview.parent().hasClass('active')) {
  1476. me.viewPreview(event);
  1477. }
  1478. event.preventDefault();
  1479. };
  1480. /**
  1481. * set the language in a cookie and reload the page
  1482. *
  1483. * @name controller.setLanguage
  1484. * @function
  1485. * @param {Event} event
  1486. */
  1487. me.setLanguage = function(event)
  1488. {
  1489. document.cookie = 'lang=' + $(event.target).data('lang');
  1490. me.reloadPage(event);
  1491. };
  1492. /**
  1493. * support input of tab character
  1494. *
  1495. * @name controller.supportTabs
  1496. * @function
  1497. * @param {Event} event
  1498. * @TODO doc what is @this here?
  1499. */
  1500. me.supportTabs = function(event)
  1501. {
  1502. var keyCode = event.keyCode || event.which;
  1503. // tab was pressed
  1504. if (keyCode === 9)
  1505. {
  1506. // prevent the textarea to lose focus
  1507. event.preventDefault();
  1508. // get caret position & selection
  1509. var val = this.value,
  1510. start = this.selectionStart,
  1511. end = this.selectionEnd;
  1512. // set textarea value to: text before caret + tab + text after caret
  1513. this.value = val.substring(0, start) + '\t' + val.substring(end);
  1514. // put caret at right position again
  1515. this.selectionStart = this.selectionEnd = start + 1;
  1516. }
  1517. };
  1518. /**
  1519. * view the editor tab
  1520. *
  1521. * @name controller.viewEditor
  1522. * @function
  1523. * @param {Event} event
  1524. */
  1525. me.viewEditor = function(event)
  1526. {
  1527. $messagePreview.parent().removeClass('active');
  1528. $messageEdit.parent().addClass('active');
  1529. $message.focus();
  1530. me.stateNewPaste();
  1531. event.preventDefault();
  1532. };
  1533. /**
  1534. * view the preview tab
  1535. *
  1536. * @name controller.viewPreview
  1537. * @function
  1538. * @param {Event} event
  1539. */
  1540. me.viewPreview = function(event)
  1541. {
  1542. $messageEdit.parent().removeClass('active');
  1543. $messagePreview.parent().addClass('active');
  1544. $message.focus();
  1545. me.stateExistingPaste(true);
  1546. me.formatPaste($('#pasteFormatter').val(), $message.val());
  1547. event.preventDefault();
  1548. };
  1549. /**
  1550. * handle history (pop) state changes
  1551. *
  1552. * currently this does only handle redirects to the home page.
  1553. *
  1554. * @name controller.historyChange
  1555. * @function
  1556. * @param {Event} event
  1557. */
  1558. me.historyChange = function(event)
  1559. {
  1560. var currentLocation = helper.scriptLocation();
  1561. if (event.originalEvent.state === null && // no state object passed
  1562. event.originalEvent.target.location.href === currentLocation && // target location is home page
  1563. window.location.href === currentLocation // and we are not already on the home page
  1564. ) {
  1565. // redirect to home page
  1566. window.location.href = currentLocation;
  1567. }
  1568. };
  1569. /**
  1570. * Forces opening the paste if the link does not do this automatically.
  1571. *
  1572. * This is necessary as browsers will not reload the page when it is
  1573. * already loaded (which is fake as it is set via history.pushState()).
  1574. *
  1575. * @name controller.pasteLinkClick
  1576. * @function
  1577. * @param {Event} event
  1578. */
  1579. me.pasteLinkClick = function(event)
  1580. {
  1581. // check if location is (already) shown in URL bar
  1582. if (window.location.href === $pasteUrl.attr('href')) {
  1583. // if so we need to load link by reloading the current site
  1584. window.location.reload(true);
  1585. }
  1586. };
  1587. /**
  1588. * create a new paste
  1589. *
  1590. * @name controller.newPaste
  1591. * @function
  1592. */
  1593. me.newPaste = function()
  1594. {
  1595. me.stateNewPaste();
  1596. me.showStatus('');
  1597. $message.text('');
  1598. me.changeBurnAfterReading();
  1599. me.changeOpenDisc();
  1600. };
  1601. /**
  1602. * removes an attachment
  1603. *
  1604. * @name controller.removeAttachment
  1605. * @function
  1606. */
  1607. me.removeAttachment = function()
  1608. {
  1609. $clonedFile.addClass('hidden');
  1610. // removes the saved decrypted file data
  1611. $attachmentLink.attr('href', '');
  1612. // the only way to deselect the file is to recreate the input // @TODO really?
  1613. $fileWrap.html($fileWrap.html());
  1614. $fileWrap.removeClass('hidden');
  1615. };
  1616. /**
  1617. * decrypt using the password from the modal dialog
  1618. *
  1619. * @name controller.decryptPasswordModal
  1620. * @function
  1621. */
  1622. me.decryptPasswordModal = function()
  1623. {
  1624. $passwordInput.val($passwordDecrypt.val());
  1625. me.displayMessages();
  1626. };
  1627. /**
  1628. * submit a password in the modal dialog
  1629. *
  1630. * @name controller.submitPasswordModal
  1631. * @function
  1632. * @param {Event} event
  1633. */
  1634. me.submitPasswordModal = function(event)
  1635. {
  1636. event.preventDefault();
  1637. $passwordModal.modal('hide');
  1638. };
  1639. /**
  1640. * display an error message,
  1641. * we use the same function for paste and reply to comments
  1642. *
  1643. * @name controller.showError
  1644. * @function
  1645. * @param {string} message - text to display
  1646. */
  1647. me.showError = function(message)
  1648. {
  1649. if ($status.length)
  1650. {
  1651. $status.addClass('errorMessage').text(message);
  1652. }
  1653. else
  1654. {
  1655. $errorMessage.removeClass('hidden');
  1656. helper.setMessage($errorMessage, message);
  1657. }
  1658. if (typeof $replyStatus !== 'undefined') {
  1659. $replyStatus.addClass('errorMessage');
  1660. $replyStatus.addClass($errorMessage.attr('class'));
  1661. if ($status.length)
  1662. {
  1663. $replyStatus.html($status.html());
  1664. }
  1665. else
  1666. {
  1667. $replyStatus.html($errorMessage.html());
  1668. }
  1669. }
  1670. };
  1671. /**
  1672. * display a status message,
  1673. * we use the same function for paste and reply to comments
  1674. *
  1675. * @name controller.showStatus
  1676. * @function
  1677. * @param {string} message - text to display
  1678. * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
  1679. */
  1680. me.showStatus = function(message, spin)
  1681. {
  1682. if (spin || false)
  1683. {
  1684. var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
  1685. $status.prepend(img);
  1686. if (typeof $replyStatus !== 'undefined') {
  1687. $replyStatus.prepend(img);
  1688. }
  1689. }
  1690. if (typeof $replyStatus !== 'undefined') {
  1691. $replyStatus.removeClass('errorMessage').text(message);
  1692. }
  1693. if (!message)
  1694. {
  1695. $status.html(' ');
  1696. return;
  1697. }
  1698. if (message === '')
  1699. {
  1700. $status.html(' ');
  1701. return;
  1702. }
  1703. $status.removeClass('errorMessage').text(message);
  1704. };
  1705. /**
  1706. * bind events to DOM elements
  1707. *
  1708. * @private
  1709. * @function
  1710. */
  1711. function bindEvents()
  1712. {
  1713. $burnAfterReading.change(me.changeBurnAfterReading);
  1714. $openDisc.change(me.changeOpenDisc);
  1715. $sendButton.click(me.sendData);
  1716. $cloneButton.click(me.clonePaste);
  1717. $rawTextButton.click(me.rawText);
  1718. $fileRemoveButton.click(me.removeAttachment);
  1719. $('.reloadlink').click(me.reloadPage);
  1720. $message.keydown(me.supportTabs);
  1721. $messageEdit.click(me.viewEditor);
  1722. $messagePreview.click(me.viewPreview);
  1723. // bootstrap template drop downs
  1724. $('ul.dropdown-menu li a', $('#expiration').parent()).click(me.setExpiration);
  1725. $('ul.dropdown-menu li a', $('#formatter').parent()).click(me.setFormat);
  1726. $('#language ul.dropdown-menu li a').click(me.setLanguage);
  1727. // page template drop down
  1728. $('#language select option').click(me.setLanguage);
  1729. // focus password input when it is shown
  1730. $passwordModal.on('shown.bs.modal', function () {
  1731. $passwordDecrypt.focus();
  1732. });
  1733. // handle modal password request on decryption
  1734. $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal);
  1735. $passwordForm.submit(me.submitPasswordModal);
  1736. $(window).on('popstate', me.historyChange);
  1737. };
  1738. /**
  1739. * main application
  1740. *
  1741. * @name controller.init
  1742. * @function
  1743. */
  1744. me.init = function()
  1745. {
  1746. // hide "no javascript" message
  1747. $('#noscript').hide();
  1748. // preload jQuery wrapped DOM elements and bind events
  1749. $attach = $('#attach');
  1750. $attachment = $('#attachment');
  1751. $attachmentLink = $('#attachment a');
  1752. $burnAfterReading = $('#burnafterreading');
  1753. $burnAfterReadingOption = $('#burnafterreadingoption');
  1754. $cipherData = $('#cipherdata');
  1755. $clearText = $('#cleartext');
  1756. $cloneButton = $('#clonebutton');
  1757. $clonedFile = $('#clonedfile');
  1758. $comments = $('#comments');
  1759. $discussion = $('#discussion');
  1760. $errorMessage = $('#errormessage');
  1761. $expiration = $('#expiration');
  1762. $fileRemoveButton = $('#fileremovebutton');
  1763. $fileWrap = $('#filewrap');
  1764. $formatter = $('#formatter');
  1765. $image = $('#image');
  1766. $loadingIndicator = $('#loadingindicator');
  1767. $message = $('#message');
  1768. $messageEdit = $('#messageedit');
  1769. $messagePreview = $('#messagepreview');
  1770. $newButton = $('#newbutton');
  1771. $openDisc = $('#opendisc');
  1772. $openDiscussion = $('#opendiscussion');
  1773. $password = $('#password');
  1774. $passwordInput = $('#passwordinput');
  1775. $passwordModal = $('#passwordmodal');
  1776. $passwordForm = $('#passwordform');
  1777. $passwordDecrypt = $('#passworddecrypt');
  1778. $pasteResult = $('#pasteresult');
  1779. // $pasteUrl is saved in sendDataContinue() if/after it is
  1780. // actually created
  1781. $prettyMessage = $('#prettymessage');
  1782. $prettyPrint = $('#prettyprint');
  1783. $preview = $('#preview');
  1784. $rawTextButton = $('#rawtextbutton');
  1785. $remainingTime = $('#remainingtime');
  1786. // $replyStatus is saved in openReply()
  1787. $sendButton = $('#sendbutton');
  1788. $status = $('#status');
  1789. bindEvents();
  1790. // display status returned by php code, if any (eg. paste was properly deleted)
  1791. if ($status.text().length > 0)
  1792. {
  1793. me.showStatus($status.text());
  1794. return;
  1795. }
  1796. // keep line height even if content empty
  1797. $status.html(' ');
  1798. // display an existing paste
  1799. if ($cipherData.text().length > 1)
  1800. {
  1801. // missing decryption key in URL?
  1802. if (window.location.hash.length === 0)
  1803. {
  1804. me.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?)'));
  1805. return;
  1806. }
  1807. // show proper elements on screen
  1808. me.stateExistingPaste();
  1809. me.displayMessages();
  1810. }
  1811. // display error message from php code
  1812. else if ($errorMessage.text().length > 1)
  1813. {
  1814. me.showError($errorMessage.text());
  1815. }
  1816. // create a new paste
  1817. else
  1818. {
  1819. me.newPaste();
  1820. }
  1821. };
  1822. return me;
  1823. })(window, document);
  1824. /**
  1825. * main application start, called when DOM is fully loaded and
  1826. * runs controller initalization after translations are loaded
  1827. */
  1828. $(i18n.loadTranslations);
  1829. return {
  1830. helper: helper,
  1831. i18n: i18n,
  1832. filter: filter,
  1833. controller: controller
  1834. };
  1835. }(jQuery, sjcl, Base64, RawDeflate);