privatebin.js 70 KB

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