privatebin.js 78 KB

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