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