privatebin.js 83 KB

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