privatebin.js 62 KB

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