privatebin.js 72 KB

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