Passed
Push — master ( 939a62...30de63 )
by El
03:51
created

js/privatebin.js (1 issue)

Severity
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.1
10
 * @name      PrivateBin
11
 * @namespace
12
 */
13
14
/** global: Base64 */
15
/** global: DOMPurify */
16
/** global: FileReader */
17
/** global: RawDeflate */
18
/** global: history */
19
/** global: navigator */
20
/** global: prettyPrint */
21
/** global: prettyPrintOne */
22
/** global: showdown */
23
/** global: sjcl */
24
/** global: kjua */
25
26
// Immediately start random number generator collector.
27
sjcl.random.startCollectors();
28
29
// main application start, called when DOM is fully loaded
30
jQuery(document).ready(function() {
31
    'use strict';
32
    // run main controller
33
    $.PrivateBin.Controller.init();
34
});
35
36
jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
37
    'use strict';
38
39
    /**
40
     * static Helper methods
41
     *
42
     * @name Helper
43
     * @class
44
     */
45
    var Helper = (function () {
46
        var me = {};
47
48
        /**
49
         * cache for script location
50
         *
51
         * @name Helper.baseUri
52
         * @private
53
         * @enum   {string|null}
54
         */
55
        var baseUri = null;
56
57
        /**
58
         * converts a duration (in seconds) into human friendly approximation
59
         *
60
         * @name Helper.secondsToHuman
61
         * @function
62
         * @param  {number} seconds
63
         * @return {Array}
64
         */
65
        me.secondsToHuman = function(seconds)
66
        {
67
            var v;
68
            if (seconds < 60)
69
            {
70
                v = Math.floor(seconds);
71
                return [v, 'second'];
72
            }
73
            if (seconds < 60 * 60)
74
            {
75
                v = Math.floor(seconds / 60);
76
                return [v, 'minute'];
77
            }
78
            if (seconds < 60 * 60 * 24)
79
            {
80
                v = Math.floor(seconds / (60 * 60));
81
                return [v, 'hour'];
82
            }
83
            // If less than 2 months, display in days:
84
            if (seconds < 60 * 60 * 24 * 60)
85
            {
86
                v = Math.floor(seconds / (60 * 60 * 24));
87
                return [v, 'day'];
88
            }
89
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
90
            return [v, 'month'];
91
        };
92
93
        /**
94
         * text range selection
95
         *
96
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
97
         * @name   Helper.selectText
98
         * @function
99
         * @param  {HTMLElement} element
100
         */
101
        me.selectText = function(element)
102
        {
103
            var range, selection;
104
105
            // MS
106
            if (document.body.createTextRange) {
107
                range = document.body.createTextRange();
108
                range.moveToElementText(element);
109
                range.select();
110
            } else if (window.getSelection) {
111
                selection = window.getSelection();
112
                range = document.createRange();
113
                range.selectNodeContents(element);
114
                selection.removeAllRanges();
115
                selection.addRange(range);
116
            }
117
        };
118
119
        /**
120
         * convert URLs to clickable links.
121
         * URLs to handle:
122
         * <pre>
123
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
124
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
125
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
126
         * </pre>
127
         *
128
         * @name   Helper.urls2links
129
         * @function
130
         * @param  {string} html
131
         * @return {string}
132
         */
133
        me.urls2links = function(html)
134
        {
135
            return html.replace(
136
                /(((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig,
137
                '<a href="$1" rel="nofollow">$1</a>'
138
            );
139
        };
140
141
        /**
142
         * minimal sprintf emulation for %s and %d formats
143
         *
144
         * Note that this function needs the parameters in the same order as the
145
         * format strings appear in the string, contrary to the original.
146
         *
147
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
148
         * @name   Helper.sprintf
149
         * @function
150
         * @param  {string} format
151
         * @param  {...*} args - one or multiple parameters injected into format string
152
         * @return {string}
153
         */
154
        me.sprintf = function()
155
        {
156
            var args = Array.prototype.slice.call(arguments);
157
            var format = args[0],
158
                i = 1;
159
            return format.replace(/%(s|d)/g, function (m) {
160
                // m is the matched format, e.g. %s, %d
161
                var val = args[i];
162
                // A switch statement so that the formatter can be extended.
163
                switch (m)
164
                {
165
                    case '%d':
166
                        val = parseFloat(val);
167
                        if (isNaN(val)) {
168
                            val = 0;
169
                        }
170
                        break;
171
                    default:
172
                        // Default is %s
173
                }
174
                ++i;
175
                return val;
176
            });
177
        };
178
179
        /**
180
         * get value of cookie, if it was set, empty string otherwise
181
         *
182
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
183
         * @name   Helper.getCookie
184
         * @function
185
         * @param  {string} cname - may not be empty
186
         * @return {string}
187
         */
188
        me.getCookie = function(cname) {
189
            var name = cname + '=',
190
                ca = document.cookie.split(';');
191
            for (var i = 0; i < ca.length; ++i) {
192
                var c = ca[i];
193
                while (c.charAt(0) === ' ')
194
                {
195
                    c = c.substring(1);
196
                }
197
                if (c.indexOf(name) === 0)
198
                {
199
                    return c.substring(name.length, c.length);
200
                }
201
            }
202
            return '';
203
        };
204
205
        /**
206
         * get the current location (without search or hash part of the URL),
207
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
208
         *
209
         * @name   Helper.baseUri
210
         * @function
211
         * @return {string}
212
         */
213
        me.baseUri = function()
214
        {
215
            // check for cached version
216
            if (baseUri !== null) {
217
                return baseUri;
218
            }
219
220
            baseUri = window.location.origin + window.location.pathname;
221
            return baseUri;
222
        };
223
224
        /**
225
         * resets state, used for unit testing
226
         *
227
         * @name   Helper.reset
228
         * @function
229
         */
230
        me.reset = function()
231
        {
232
            baseUri = null;
233
        };
234
235
        return me;
236
    })();
237
238
    /**
239
     * internationalization module
240
     *
241
     * @name I18n
242
     * @class
243
     */
244
    var I18n = (function () {
245
        var me = {};
246
247
        /**
248
         * const for string of loaded language
249
         *
250
         * @name I18n.languageLoadedEvent
251
         * @private
252
         * @prop   {string}
253
         * @readonly
254
         */
255
        var languageLoadedEvent = 'languageLoaded';
256
257
        /**
258
         * supported languages, minus the built in 'en'
259
         *
260
         * @name I18n.supportedLanguages
261
         * @private
262
         * @prop   {string[]}
263
         * @readonly
264
         */
265
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
266
267
        /**
268
         * built in language
269
         *
270
         * @name I18n.language
271
         * @private
272
         * @prop   {string|null}
273
         */
274
        var language = null;
275
276
        /**
277
         * translation cache
278
         *
279
         * @name I18n.translations
280
         * @private
281
         * @enum   {Object}
282
         */
283
        var translations = {};
284
285
        /**
286
         * translate a string, alias for I18n.translate
287
         *
288
         * @name   I18n._
289
         * @function
290
         * @param  {jQuery} $element - optional
291
         * @param  {string} messageId
292
         * @param  {...*} args - one or multiple parameters injected into placeholders
293
         * @return {string}
294
         */
295
        me._ = function()
296
        {
297
            return me.translate.apply(this, arguments);
298
        };
299
300
        /**
301
         * translate a string
302
         *
303
         * Optionally pass a jQuery element as the first parameter, to automatically
304
         * let the text of this element be replaced. In case the (asynchronously
305
         * loaded) language is not downloadet yet, this will make sure the string
306
         * is replaced when it is actually loaded.
307
         * So for easy translations passing the jQuery object to apply it to is
308
         * more save, especially when they are loaded in the beginning.
309
         *
310
         * @name   I18n.translate
311
         * @function
312
         * @param  {jQuery} $element - optional
313
         * @param  {string} messageId
314
         * @param  {...*} args - one or multiple parameters injected into placeholders
315
         * @return {string}
316
         */
317
        me.translate = function()
318
        {
319
            // convert parameters to array
320
            var args = Array.prototype.slice.call(arguments),
321
                messageId,
322
                $element = null;
323
324
            // parse arguments
325
            if (args[0] instanceof jQuery) {
326
                // optional jQuery element as first parameter
327
                $element = args[0];
328
                args.shift();
329
            }
330
331
            // extract messageId from arguments
332
            var usesPlurals = $.isArray(args[0]);
333
            if (usesPlurals) {
334
                // use the first plural form as messageId, otherwise the singular
335
                messageId = args[0].length > 1 ? args[0][1] : args[0][0];
336
            } else {
337
                messageId = args[0];
338
            }
339
340
            if (messageId.length === 0) {
341
                return messageId;
342
            }
343
344
            // if no translation string cannot be found (in translations object)
345
            if (!translations.hasOwnProperty(messageId) || language === null) {
346
                // if language is still loading and we have an elemt assigned
347
                if (language === null && $element !== null) {
348
                    // handle the error by attaching the language loaded event
349
                    var orgArguments = arguments;
350
                    $(document).on(languageLoadedEvent, function () {
351
                        // log to show that the previous error could be mitigated
352
                        console.warn('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language);
353
                        // re-execute this function
354
                        me.translate.apply(this, orgArguments);
355
                    });
356
357
                    // and fall back to English for now until the real language
358
                    // file is loaded
359
                }
360
361
                // for all other langauges than English for which this behaviour
362
                // is expected as it is built-in, log error
363
                if (language !== null && language !== 'en') {
364
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
365
                    // fallback to English
366
                }
367
368
                // save English translation (should be the same on both sides)
369
                translations[messageId] = args[0];
370
            }
371
372
            // lookup plural translation
373
            if (usesPlurals && $.isArray(translations[messageId])) {
374
                var n = parseInt(args[1] || 1, 10),
375
                    key = me.getPluralForm(n),
376
                    maxKey = translations[messageId].length - 1;
377
                if (key > maxKey) {
378
                    key = maxKey;
379
                }
380
                args[0] = translations[messageId][key];
381
                args[1] = n;
382
            } else {
383
                // lookup singular translation
384
                args[0] = translations[messageId];
385
            }
386
387
            // format string
388
            var output = Helper.sprintf.apply(this, args);
389
390
            // if $element is given, apply text to element
391
            if ($element !== null) {
392
                // get last text node of element
393
                var content = $element.contents();
394
                if (content.length > 1) {
395
                    content[content.length - 1].nodeValue = ' ' + output;
396
                } else {
397
                    $element.text(output);
398
                }
399
            }
400
401
            return output;
402
        };
403
404
        /**
405
         * per language functions to use to determine the plural form
406
         *
407
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
408
         * @name   I18n.getPluralForm
409
         * @function
410
         * @param  {int} n
411
         * @return {int} array key
412
         */
413
        me.getPluralForm = function(n) {
414
            switch (language)
415
            {
416
                case 'fr':
417
                case 'oc':
418
                case 'zh':
419
                    return n > 1 ? 1 : 0;
420
                case 'pl':
421
                    return n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
422
                case 'ru':
423
                    return n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
424
                case 'sl':
425
                    return n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0));
426
                // de, en, es, it, no, pt
427
                default:
428
                    return n !== 1 ? 1 : 0;
429
            }
430
        };
431
432
        /**
433
         * load translations into cache
434
         *
435
         * @name   I18n.loadTranslations
436
         * @function
437
         */
438
        me.loadTranslations = function()
439
        {
440
            var newLanguage = Helper.getCookie('lang');
441
442
            // auto-select language based on browser settings
443
            if (newLanguage.length === 0) {
444
                newLanguage = (navigator.language || navigator.userLanguage || 'en').substring(0, 2);
445
            }
446
447
            // if language is already used skip update
448
            if (newLanguage === language) {
449
                return;
450
            }
451
452
            // if language is built-in (English) skip update
453
            if (newLanguage === 'en') {
454
                language = 'en';
455
                return;
456
            }
457
458
            // if language is not supported, show error
459
            if (supportedLanguages.indexOf(newLanguage) === -1) {
460
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
461
                language = 'en';
462
                return;
463
            }
464
465
            // load strings from JSON
466
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
467
                language = newLanguage;
468
                translations = data;
469
                $(document).triggerHandler(languageLoadedEvent);
470
            }).fail(function (data, textStatus, errorMsg) {
471
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
472
                language = 'en';
473
            });
474
        };
475
476
        /**
477
         * resets state, used for unit testing
478
         *
479
         * @name   I18n.reset
480
         * @function
481
         */
482
        me.reset = function(mockLanguage, mockTranslations)
483
        {
484
            language = mockLanguage || null;
485
            translations = mockTranslations || {};
486
        };
487
488
        return me;
489
    })();
490
491
    /**
492
     * handles everything related to en/decryption
493
     *
494
     * @name CryptTool
495
     * @class
496
     */
497
    var CryptTool = (function () {
498
        var me = {};
499
500
        /**
501
         * compress a message (deflate compression), returns base64 encoded data
502
         *
503
         * @name   CryptTool.compress
504
         * @function
505
         * @private
506
         * @param  {string} message
507
         * @return {string} base64 data
508
         */
509
        function compress(message)
510
        {
511
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
512
        }
513
514
        /**
515
         * decompress a message compressed with cryptToolcompress()
516
         *
517
         * @name   CryptTool.decompress
518
         * @function
519
         * @private
520
         * @param  {string} data - base64 data
521
         * @return {string} message
522
         */
523
        function decompress(data)
524
        {
525
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
526
        }
527
528
        /**
529
         * compress, then encrypt message with given key and password
530
         *
531
         * @name   CryptTool.cipher
532
         * @function
533
         * @param  {string} key
534
         * @param  {string} password
535
         * @param  {string} message
536
         * @return {string} data - JSON with encrypted data
537
         */
538
        me.cipher = function(key, password, message)
539
        {
540
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
541
            var options = {
542
                mode: 'gcm',
543
                ks: 256,
544
                ts: 128
545
            };
546
547
            if ((password || '').trim().length === 0) {
548
                return sjcl.encrypt(key, compress(message), options);
549
            }
550
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
551
        };
552
553
        /**
554
         * decrypt message with key, then decompress
555
         *
556
         * @name   CryptTool.decipher
557
         * @function
558
         * @param  {string} key
559
         * @param  {string} password
560
         * @param  {string} data - JSON with encrypted data
561
         * @return {string} decrypted message, empty if decryption failed
562
         */
563
        me.decipher = function(key, password, data)
564
        {
565
            if (data !== undefined) {
566
                try {
567
                    return decompress(sjcl.decrypt(key, data));
568
                } catch(err) {
569
                    try {
570
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
571
                    } catch(e) {
572
                        return '';
573
                    }
574
                }
575
            }
576
        };
577
578
        /**
579
         * checks whether the crypt tool has collected enough entropy
580
         *
581
         * @name   CryptTool.isEntropyReady
582
         * @function
583
         * @return {bool}
584
         */
585
        me.isEntropyReady = function()
586
        {
587
            return sjcl.random.isReady();
588
        };
589
590
        /**
591
         * add a listener function, triggered when enough entropy is available
592
         *
593
         * @name   CryptTool.addEntropySeedListener
594
         * @function
595
         * @param {function} func
596
         */
597
        me.addEntropySeedListener = function(func)
598
        {
599
            sjcl.random.addEventListener('seeded', func);
600
        };
601
602
        /**
603
         * returns a random symmetric key
604
         *
605
         * @name   CryptTool.getSymmetricKey
606
         * @function
607
         * @return {string} func
608
         */
609
        me.getSymmetricKey = function()
610
        {
611
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
612
        };
613
614
        return me;
615
    })();
616
617
    /**
618
     * (Model) Data source (aka MVC)
619
     *
620
     * @name   Model
621
     * @class
622
     */
623
    var Model = (function () {
624
        var me = {};
625
626
        var $cipherData,
627
            $templates;
628
629
        var id = null, symmetricKey = null;
630
631
        /**
632
         * returns the expiration set in the HTML
633
         *
634
         * @name   Model.getExpirationDefault
635
         * @function
636
         * @return string
637
         */
638
        me.getExpirationDefault = function()
639
        {
640
            return $('#pasteExpiration').val();
641
        };
642
643
        /**
644
         * returns the format set in the HTML
645
         *
646
         * @name   Model.getFormatDefault
647
         * @function
648
         * @return string
649
         */
650
        me.getFormatDefault = function()
651
        {
652
            return $('#pasteFormatter').val();
653
        };
654
655
        /**
656
         * check if cipher data was supplied
657
         *
658
         * @name   Model.getCipherData
659
         * @function
660
         * @return boolean
661
         */
662
        me.hasCipherData = function()
663
        {
664
            return me.getCipherData().length > 0;
665
        };
666
667
        /**
668
         * returns the cipher data
669
         *
670
         * @name   Model.getCipherData
671
         * @function
672
         * @return string
673
         */
674
        me.getCipherData = function()
675
        {
676
            return $cipherData.text();
677
        };
678
679
        /**
680
         * get the pastes unique identifier from the URL,
681
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
682
         *
683
         * @name   Model.getPasteId
684
         * @function
685
         * @return {string} unique identifier
686
         * @throws {string}
687
         */
688
        me.getPasteId = function()
689
        {
690
            if (id === null) {
691
                id = window.location.search.substring(1);
692
693
                if (id === '') {
694
                    throw 'no paste id given';
695
                }
696
            }
697
698
            return id;
699
        };
700
701
        /**
702
         * return the deciphering key stored in anchor part of the URL
703
         *
704
         * @name   Model.getPasteKey
705
         * @function
706
         * @return {string|null} key
707
         * @throws {string}
708
         */
709
        me.getPasteKey = function()
710
        {
711
            if (symmetricKey === null) {
712
                symmetricKey = window.location.hash.substring(1);
713
714
                if (symmetricKey === '') {
715
                    throw 'no encryption key given';
716
                }
717
718
                // Some web 2.0 services and redirectors add data AFTER the anchor
719
                // (such as &utm_source=...). We will strip any additional data.
720
                var ampersandPos = symmetricKey.indexOf('&');
721
                if (ampersandPos > -1)
722
                {
723
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
724
                }
725
            }
726
727
            return symmetricKey;
728
        };
729
730
        /**
731
         * returns a jQuery copy of the HTML template
732
         *
733
         * @name Model.getTemplate
734
         * @function
735
         * @param  {string} name - the name of the template
736
         * @return {jQuery}
737
         */
738
        me.getTemplate = function(name)
739
        {
740
            // find template
741
            var $element = $templates.find('#' + name + 'template').clone(true);
742
            // change ID to avoid collisions (one ID should really be unique)
743
            return $element.prop('id', name);
744
        };
745
746
        /**
747
         * resets state, used for unit testing
748
         *
749
         * @name   Model.reset
750
         * @function
751
         */
752
        me.reset = function()
753
        {
754
            $cipherData = $templates = id = symmetricKey = null;
755
        };
756
757
        /**
758
         * init navigation manager
759
         *
760
         * preloads jQuery elements
761
         *
762
         * @name   Model.init
763
         * @function
764
         */
765
        me.init = function()
766
        {
767
            $cipherData = $('#cipherdata');
768
            $templates = $('#templates');
769
        };
770
771
        return me;
772
    })();
773
774
    /**
775
     * Helper functions for user interface
776
     *
777
     * everything directly UI-related, which fits nowhere else
778
     *
779
     * @name   UiHelper
780
     * @class
781
     */
782
    var UiHelper = (function () {
783
        var me = {};
784
785
        /**
786
         * handle history (pop) state changes
787
         *
788
         * currently this does only handle redirects to the home page.
789
         *
790
         * @name   UiHelper.historyChange
791
         * @private
792
         * @function
793
         * @param  {Event} event
794
         */
795
        function historyChange(event)
796
        {
797
            var currentLocation = Helper.baseUri();
798
            if (event.originalEvent.state === null && // no state object passed
799
                event.target.location.href === currentLocation && // target location is home page
800
                window.location.href === currentLocation // and we are not already on the home page
801
            ) {
802
                // redirect to home page
803
                window.location.href = currentLocation;
804
            }
805
        }
806
807
        /**
808
         * reload the page
809
         *
810
         * This takes the user to the PrivateBin homepage.
811
         *
812
         * @name   UiHelper.reloadHome
813
         * @function
814
         */
815
        me.reloadHome = function()
816
        {
817
            window.location.href = Helper.baseUri();
818
        };
819
820
        /**
821
         * checks whether the element is currently visible in the viewport (so
822
         * the user can actually see it)
823
         *
824
         * @see    {@link https://stackoverflow.com/a/40658647}
825
         * @name   UiHelper.isVisible
826
         * @function
827
         * @param  {jQuery} $element The link hash to move to.
828
         */
829
        me.isVisible = function($element)
830
        {
831
            var elementTop = $element.offset().top;
832
            var viewportTop = $(window).scrollTop();
833
            var viewportBottom = viewportTop + $(window).height();
834
835
            return elementTop > viewportTop && elementTop < viewportBottom;
836
        };
837
838
        /**
839
         * scrolls to a specific element
840
         *
841
         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
842
         * @name   UiHelper.scrollTo
843
         * @function
844
         * @param  {jQuery}           $element        The link hash to move to.
845
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
846
         * @param  {string}           animationEffect   passed to jQuery .animate
847
         * @param  {function}         finishedCallback  function to call after animation finished
848
         */
849
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
850
        {
851
            var $body = $('html, body'),
852
                margin = 50,
853
                callbackCalled = false;
854
855
            //calculate destination place
856
            var dest = 0;
857
            // if it would scroll out of the screen at the bottom only scroll it as
858
            // far as the screen can go
859
            if ($element.offset().top > $(document).height() - $(window).height()) {
860
                dest = $(document).height() - $(window).height();
861
            } else {
862
                dest = $element.offset().top - margin;
863
            }
864
            // skip animation if duration is set to 0
865
            if (animationDuration === 0) {
866
                window.scrollTo(0, dest);
867
            } else {
868
                // stop previous animation
869
                $body.stop();
870
                // scroll to destination
871
                $body.animate({
872
                    scrollTop: dest
873
                }, animationDuration, animationEffect);
874
            }
875
876
            // as we have finished we can enable scrolling again
877
            $body.queue(function (next) {
878
                if (!callbackCalled) {
879
                    // call user function if needed
880
                    if (typeof finishedCallback !== 'undefined') {
881
                        finishedCallback();
882
                    }
883
884
                    // prevent calling this function twice
885
                    callbackCalled = true;
886
                }
887
                next();
888
            });
889
        };
890
891
        /**
892
         * trigger a history (pop) state change
893
         *
894
         * used to test the UiHelper.historyChange private function
895
         *
896
         * @name   UiHelper.mockHistoryChange
897
         * @function
898
         * @param  {string} state   (optional) state to mock
899
         */
900
        me.mockHistoryChange = function(state)
901
        {
902
            if (typeof state === 'undefined') {
903
                state = null;
904
            }
905
            historyChange($.Event('popstate', {originalEvent: new PopStateEvent('popstate', {state: state}), target: window}));
906
        };
907
908
        /**
909
         * initialize
910
         *
911
         * @name   UiHelper.init
912
         * @function
913
         */
914
        me.init = function()
915
        {
916
            // update link to home page
917
            $('.reloadlink').prop('href', Helper.baseUri());
918
919
            $(window).on('popstate', historyChange);
920
        };
921
922
        return me;
923
    })();
924
925
    /**
926
     * Alert/error manager
927
     *
928
     * @name   Alert
929
     * @class
930
     */
931
    var Alert = (function () {
932
        var me = {};
933
934
        var $errorMessage,
935
            $loadingIndicator,
936
            $statusMessage,
937
            $remainingTime;
938
939
        var currentIcon;
940
941
        var alertType = [
942
            'loading', // not in bootstrap, but using a good value here
943
            'info', // status icon
944
            'warning', // not used yet
945
            'danger' // error icon
946
        ];
947
948
        var customHandler;
949
950
        /**
951
         * forwards a request to the i18n module and shows the element
952
         *
953
         * @name   Alert.handleNotification
954
         * @private
955
         * @function
956
         * @param  {int} id - id of notification
957
         * @param  {jQuery} $element - jQuery object
958
         * @param  {string|array} args
959
         * @param  {string|null} icon - optional, icon
960
         */
961
        function handleNotification(id, $element, args, icon)
962
        {
963
            // basic parsing/conversion of parameters
964
            if (typeof icon === 'undefined') {
965
                icon = null;
966
            }
967
            if (typeof args === 'undefined') {
968
                args = null;
969
            } else if (typeof args === 'string') {
970
                // convert string to array if needed
971
                args = [args];
972
            }
973
974
            // pass to custom handler if defined
975
            if (typeof customHandler === 'function') {
976
                var handlerResult = customHandler(alertType[id], $element, args, icon);
977
                if (handlerResult === true) {
978
                    // if it returns true, skip own handler
979
                    return;
980
                }
981
                if (handlerResult instanceof jQuery) {
982
                    // continue processing with new element
983
                    $element = handlerResult;
984
                    icon = null; // icons not supported in this case
985
                }
986
            }
987
988
            // handle icon
989
            if (icon !== null && // icon was passed
990
                icon !== currentIcon[id] // and it differs from current icon
991
            ) {
992
                var $glyphIcon = $element.find(':first');
993
994
                // remove (previous) icon
995
                $glyphIcon.removeClass(currentIcon[id]);
996
997
                // any other thing as a string (e.g. 'null') (only) removes the icon
998
                if (typeof icon === 'string') {
999
                    // set new icon
1000
                    currentIcon[id] = 'glyphicon-' + icon;
1001
                    $glyphIcon.addClass(currentIcon[id]);
1002
                }
1003
            }
1004
1005
            // show text
1006
            if (args !== null) {
1007
                // add jQuery object to it as first parameter
1008
                args.unshift($element);
1009
                // pass it to I18n
1010
                I18n._.apply(this, args);
1011
            }
1012
1013
            // show notification
1014
            $element.removeClass('hidden');
1015
        }
1016
1017
        /**
1018
         * display a status message
1019
         *
1020
         * This automatically passes the text to I18n for translation.
1021
         *
1022
         * @name   Alert.showStatus
1023
         * @function
1024
         * @param  {string|array} message     string, use an array for %s/%d options
1025
         * @param  {string|null}  icon        optional, the icon to show,
1026
         *                                    default: leave previous icon
1027
         */
1028
        me.showStatus = function(message, icon)
1029
        {
1030
            console.info('status shown: ', message);
1031
            handleNotification(1, $statusMessage, message, icon);
1032
        };
1033
1034
        /**
1035
         * display an error message
1036
         *
1037
         * This automatically passes the text to I18n for translation.
1038
         *
1039
         * @name   Alert.showError
1040
         * @function
1041
         * @param  {string|array} message     string, use an array for %s/%d options
1042
         * @param  {string|null}  icon        optional, the icon to show, default:
1043
         *                                    leave previous icon
1044
         */
1045
        me.showError = function(message, icon)
1046
        {
1047
            console.error('error message shown: ', message);
1048
            handleNotification(3, $errorMessage, message, icon);
1049
        };
1050
1051
        /**
1052
         * display remaining message
1053
         *
1054
         * This automatically passes the text to I18n for translation.
1055
         *
1056
         * @name   Alert.showRemaining
1057
         * @function
1058
         * @param  {string|array} message     string, use an array for %s/%d options
1059
         */
1060
        me.showRemaining = function(message)
1061
        {
1062
            console.info('remaining message shown: ', message);
1063
            handleNotification(1, $remainingTime, message);
1064
        };
1065
1066
        /**
1067
         * shows a loading message, optionally with a percentage
1068
         *
1069
         * This automatically passes all texts to the i10s module.
1070
         *
1071
         * @name   Alert.showLoading
1072
         * @function
1073
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1074
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1075
         */
1076
        me.showLoading = function(message, icon)
1077
        {
1078
            if (typeof message !== 'undefined' && message !== null) {
1079
                console.info('status changed: ', message);
1080
            }
1081
1082
            // default message text
1083
            if (typeof message === 'undefined') {
1084
                message = 'Loading…';
1085
            }
1086
1087
            handleNotification(0, $loadingIndicator, message, icon);
1088
1089
            // show loading status (cursor)
1090
            $('body').addClass('loading');
1091
        };
1092
1093
        /**
1094
         * hides the loading message
1095
         *
1096
         * @name   Alert.hideLoading
1097
         * @function
1098
         */
1099
        me.hideLoading = function()
1100
        {
1101
            $loadingIndicator.addClass('hidden');
1102
1103
            // hide loading cursor
1104
            $('body').removeClass('loading');
1105
        };
1106
1107
        /**
1108
         * hides any status/error messages
1109
         *
1110
         * This does not include the loading message.
1111
         *
1112
         * @name   Alert.hideMessages
1113
         * @function
1114
         */
1115
        me.hideMessages = function()
1116
        {
1117
            // also possible: $('.statusmessage').addClass('hidden');
1118
            $statusMessage.addClass('hidden');
1119
            $errorMessage.addClass('hidden');
1120
        };
1121
1122
        /**
1123
         * set a custom handler, which gets all notifications.
1124
         *
1125
         * This handler gets the following arguments:
1126
         * alertType (see array), $element, args, icon
1127
         * If it returns true, the own processing will be stopped so the message
1128
         * will not be displayed. Otherwise it will continue.
1129
         * As an aditional feature it can return q jQuery element, which will
1130
         * then be used to add the message there. Icons are not supported in
1131
         * that case and will be ignored.
1132
         * Pass 'null' to reset/delete the custom handler.
1133
         * Note that there is no notification when a message is supposed to get
1134
         * hidden.
1135
         *
1136
         * @name   Alert.setCustomHandler
1137
         * @function
1138
         * @param {function|null} newHandler
1139
         */
1140
        me.setCustomHandler = function(newHandler)
1141
        {
1142
            customHandler = newHandler;
1143
        };
1144
1145
        /**
1146
         * init status manager
1147
         *
1148
         * preloads jQuery elements
1149
         *
1150
         * @name   Alert.init
1151
         * @function
1152
         */
1153
        me.init = function()
1154
        {
1155
            // hide "no javascript" error message
1156
            $('#noscript').hide();
1157
1158
            // not a reset, but first set of the elements
1159
            $errorMessage = $('#errormessage');
1160
            $loadingIndicator = $('#loadingindicator');
1161
            $statusMessage = $('#status');
1162
            $remainingTime = $('#remainingtime');
1163
1164
            currentIcon = [
1165
                'glyphicon-time', // loading icon
1166
                'glyphicon-info-sign', // status icon
1167
                '', // reserved for warning, not used yet
1168
                'glyphicon-alert' // error icon
1169
            ];
1170
        };
1171
1172
        return me;
1173
    })();
1174
1175
    /**
1176
     * handles paste status/result
1177
     *
1178
     * @name   PasteStatus
1179
     * @class
1180
     */
1181
    var PasteStatus = (function () {
1182
        var me = {};
1183
1184
        var $pasteSuccess,
1185
            $pasteUrl,
1186
            $remainingTime,
1187
            $shortenButton;
1188
1189
        /**
1190
         * forward to URL shortener
1191
         *
1192
         * @name   PasteStatus.sendToShortener
1193
         * @private
1194
         * @function
1195
         */
1196
        function sendToShortener()
1197
        {
1198
            window.location.href = $shortenButton.data('shortener') +
1199
                                   encodeURIComponent($pasteUrl.attr('href'));
1200
        }
1201
1202
        /**
1203
         * Forces opening the paste if the link does not do this automatically.
1204
         *
1205
         * This is necessary as browsers will not reload the page when it is
1206
         * already loaded (which is fake as it is set via history.pushState()).
1207
         *
1208
         * @name   PasteStatus.pasteLinkClick
1209
         * @function
1210
         */
1211
        function pasteLinkClick()
1212
        {
1213
            // check if location is (already) shown in URL bar
1214
            if (window.location.href === $pasteUrl.attr('href')) {
1215
                // if so we need to load link by reloading the current site
1216
                window.location.reload(true);
1217
            }
1218
        }
1219
1220
        /**
1221
         * creates a notification after a successfull paste upload
1222
         *
1223
         * @name   PasteStatus.createPasteNotification
1224
         * @function
1225
         * @param  {string} url
1226
         * @param  {string} deleteUrl
1227
         */
1228
        me.createPasteNotification = function(url, deleteUrl)
1229
        {
1230
            $('#pastelink').html(
1231
                I18n._(
1232
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1233
                    url, url
1234
                )
1235
            );
1236
            // save newly created element
1237
            $pasteUrl = $('#pasteurl');
1238
            // and add click event
1239
            $pasteUrl.click(pasteLinkClick);
1240
1241
            // shorten button
1242
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1243
1244
            // show result
1245
            $pasteSuccess.removeClass('hidden');
1246
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1247
            Helper.selectText($pasteUrl[0]);
1248
        };
1249
1250
        /**
1251
         * shows the remaining time
1252
         *
1253
         * @name PasteStatus.showRemainingTime
1254
         * @function
1255
         * @param {object} pasteMetaData
1256
         */
1257
        me.showRemainingTime = function(pasteMetaData)
1258
        {
1259
            if (pasteMetaData.burnafterreading) {
1260
                // display paste "for your eyes only" if it is deleted
1261
1262
                // actually remove paste, before we claim it is deleted
1263
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1264
1265
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1266
                $remainingTime.addClass('foryoureyesonly');
1267
1268
                // discourage cloning (it cannot really be prevented)
1269
                TopNav.hideCloneButton();
1270
1271
            } else if (pasteMetaData.expire_date) {
1272
                // display paste expiration
1273
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1274
                    expirationLabel = [
1275
                        'This document will expire in %d ' + expiration[1] + '.',
1276
                        'This document will expire in %d ' + expiration[1] + 's.'
1277
                    ];
1278
1279
                Alert.showRemaining([expirationLabel, expiration[0]]);
1280
                $remainingTime.removeClass('foryoureyesonly');
1281
            } else {
1282
                // never expires
1283
                return;
1284
            }
1285
1286
            // in the end, display notification
1287
            $remainingTime.removeClass('hidden');
1288
        };
1289
1290
        /**
1291
         * hides the remaining time and successful upload notification
1292
         *
1293
         * @name PasteStatus.hideMessages
1294
         * @function
1295
         */
1296
        me.hideMessages = function()
1297
        {
1298
            $remainingTime.addClass('hidden');
1299
            $pasteSuccess.addClass('hidden');
1300
        };
1301
1302
        /**
1303
         * init status manager
1304
         *
1305
         * preloads jQuery elements
1306
         *
1307
         * @name   PasteStatus.init
1308
         * @function
1309
         */
1310
        me.init = function()
1311
        {
1312
            $pasteSuccess = $('#pastesuccess');
1313
            // $pasteUrl is saved in me.createPasteNotification() after creation
1314
            $remainingTime = $('#remainingtime');
1315
            $shortenButton = $('#shortenbutton');
1316
1317
            // bind elements
1318
            $shortenButton.click(sendToShortener);
1319
        };
1320
1321
        return me;
1322
    })();
1323
1324
    /**
1325
     * password prompt
1326
     *
1327
     * @name Prompt
1328
     * @class
1329
     */
1330
    var Prompt = (function () {
1331
        var me = {};
1332
1333
        var $passwordDecrypt,
1334
            $passwordForm,
1335
            $passwordModal;
1336
1337
        var password = '';
1338
1339
        /**
1340
         * submit a password in the modal dialog
1341
         *
1342
         * @name Prompt.submitPasswordModal
1343
         * @private
1344
         * @function
1345
         * @param  {Event} event
1346
         */
1347
        function submitPasswordModal(event)
1348
        {
1349
            event.preventDefault();
1350
1351
            // get input
1352
            password = $passwordDecrypt.val();
1353
1354
            // hide modal
1355
            $passwordModal.modal('hide');
1356
1357
            PasteDecrypter.run();
1358
        }
1359
1360
        /**
1361
         * ask the user for the password and set it
1362
         *
1363
         * @name Prompt.requestPassword
1364
         * @function
1365
         */
1366
        me.requestPassword = function()
1367
        {
1368
            // show new bootstrap method (if available)
1369
            if ($passwordModal.length !== 0) {
1370
                $passwordModal.modal({
1371
                    backdrop: 'static',
1372
                    keyboard: false
1373
                });
1374
                return;
1375
            }
1376
1377
            // fallback to old method for page template
1378
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1379
            if (newPassword === null) {
1380
                throw 'password prompt canceled';
1381
            }
1382
            if (password.length === 0) {
1383
                // recurse…
1384
                return me.requestPassword();
1385
            }
1386
1387
            password = newPassword;
1388
        };
1389
1390
        /**
1391
         * get the cached password
1392
         *
1393
         * If you do not get a password with this function
1394
         * (returns an empty string), use requestPassword.
1395
         *
1396
         * @name   Prompt.getPassword
1397
         * @function
1398
         * @return {string}
1399
         */
1400
        me.getPassword = function()
1401
        {
1402
            return password;
1403
        };
1404
1405
        /**
1406
         * init status manager
1407
         *
1408
         * preloads jQuery elements
1409
         *
1410
         * @name   Prompt.init
1411
         * @function
1412
         */
1413
        me.init = function()
1414
        {
1415
            $passwordDecrypt = $('#passworddecrypt');
1416
            $passwordForm = $('#passwordform');
1417
            $passwordModal = $('#passwordmodal');
1418
1419
            // bind events
1420
1421
            // focus password input when it is shown
1422
            $passwordModal.on('shown.bs.Model', function () {
1423
                $passwordDecrypt.focus();
1424
            });
1425
            // handle Model password submission
1426
            $passwordForm.submit(submitPasswordModal);
1427
        };
1428
1429
        return me;
1430
    })();
1431
1432
    /**
1433
     * Manage paste/message input, and preview tab
1434
     *
1435
     * Note that the actual preview is handled by PasteViewer.
1436
     *
1437
     * @name   Editor
1438
     * @class
1439
     */
1440
    var Editor = (function () {
1441
        var me = {};
1442
1443
        var $editorTabs,
1444
            $messageEdit,
1445
            $messagePreview,
1446
            $message;
1447
1448
        var isPreview = false;
1449
1450
        /**
1451
         * support input of tab character
1452
         *
1453
         * @name   Editor.supportTabs
1454
         * @function
1455
         * @param  {Event} event
1456
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1457
         */
1458
        function supportTabs(event)
1459
        {
1460
            var keyCode = event.keyCode || event.which;
1461
            // tab was pressed
1462
            if (keyCode === 9) {
1463
                // get caret position & selection
1464
                var val   = this.value,
1465
                    start = this.selectionStart,
1466
                    end   = this.selectionEnd;
1467
                // set textarea value to: text before caret + tab + text after caret
1468
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1469
                // put caret at right position again
1470
                this.selectionStart = this.selectionEnd = start + 1;
1471
                // prevent the textarea to lose focus
1472
                event.preventDefault();
1473
            }
1474
        }
1475
1476
        /**
1477
         * view the Editor tab
1478
         *
1479
         * @name   Editor.viewEditor
1480
         * @function
1481
         * @param  {Event} event - optional
1482
         */
1483
        function viewEditor(event)
1484
        {
1485
            // toggle buttons
1486
            $messageEdit.addClass('active');
1487
            $messagePreview.removeClass('active');
1488
1489
            PasteViewer.hide();
1490
1491
            // reshow input
1492
            $message.removeClass('hidden');
1493
1494
            me.focusInput();
1495
1496
            // finish
1497
            isPreview = false;
1498
1499
            // prevent jumping of page to top
1500
            if (typeof event !== 'undefined') {
1501
                event.preventDefault();
1502
            }
1503
        }
1504
1505
        /**
1506
         * view the preview tab
1507
         *
1508
         * @name   Editor.viewPreview
1509
         * @function
1510
         * @param  {Event} event
1511
         */
1512
        function viewPreview(event)
1513
        {
1514
            // toggle buttons
1515
            $messageEdit.removeClass('active');
1516
            $messagePreview.addClass('active');
1517
1518
            // hide input as now preview is shown
1519
            $message.addClass('hidden');
1520
1521
            // show preview
1522
            PasteViewer.setText($message.val());
1523
            PasteViewer.run();
1524
1525
            // finish
1526
            isPreview = true;
1527
1528
            // prevent jumping of page to top
1529
            if (typeof event !== 'undefined') {
1530
                event.preventDefault();
1531
            }
1532
        }
1533
1534
        /**
1535
         * get the state of the preview
1536
         *
1537
         * @name   Editor.isPreview
1538
         * @function
1539
         */
1540
        me.isPreview = function()
1541
        {
1542
            return isPreview;
1543
        };
1544
1545
        /**
1546
         * reset the Editor view
1547
         *
1548
         * @name   Editor.resetInput
1549
         * @function
1550
         */
1551
        me.resetInput = function()
1552
        {
1553
            // go back to input
1554
            if (isPreview) {
1555
                viewEditor();
1556
            }
1557
1558
            // clear content
1559
            $message.val('');
1560
        };
1561
1562
        /**
1563
         * shows the Editor
1564
         *
1565
         * @name   Editor.show
1566
         * @function
1567
         */
1568
        me.show = function()
1569
        {
1570
            $message.removeClass('hidden');
1571
            $editorTabs.removeClass('hidden');
1572
        };
1573
1574
        /**
1575
         * hides the Editor
1576
         *
1577
         * @name   Editor.reset
1578
         * @function
1579
         */
1580
        me.hide = function()
1581
        {
1582
            $message.addClass('hidden');
1583
            $editorTabs.addClass('hidden');
1584
        };
1585
1586
        /**
1587
         * focuses the message input
1588
         *
1589
         * @name   Editor.focusInput
1590
         * @function
1591
         */
1592
        me.focusInput = function()
1593
        {
1594
            $message.focus();
1595
        };
1596
1597
        /**
1598
         * sets a new text
1599
         *
1600
         * @name   Editor.setText
1601
         * @function
1602
         * @param {string} newText
1603
         */
1604
        me.setText = function(newText)
1605
        {
1606
            $message.val(newText);
1607
        };
1608
1609
        /**
1610
         * returns the current text
1611
         *
1612
         * @name   Editor.getText
1613
         * @function
1614
         * @return {string}
1615
         */
1616
        me.getText = function()
1617
        {
1618
            return $message.val();
1619
        };
1620
1621
        /**
1622
         * init status manager
1623
         *
1624
         * preloads jQuery elements
1625
         *
1626
         * @name   Editor.init
1627
         * @function
1628
         */
1629
        me.init = function()
1630
        {
1631
            $editorTabs = $('#editorTabs');
1632
            $message = $('#message');
1633
1634
            // bind events
1635
            $message.keydown(supportTabs);
1636
1637
            // bind click events to tab switchers (a), but save parent of them
1638
            // (li)
1639
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1640
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1641
        };
1642
1643
        return me;
1644
    })();
1645
1646
    /**
1647
     * (view) Parse and show paste.
1648
     *
1649
     * @name   PasteViewer
1650
     * @class
1651
     */
1652
    var PasteViewer = (function () {
1653
        var me = {};
1654
1655
        var $placeholder,
1656
            $prettyMessage,
1657
            $prettyPrint,
1658
            $plainText;
1659
1660
        var text,
1661
            format = 'plaintext',
1662
            isDisplayed = false,
1663
            isChanged = true; // by default true as nothing was parsed yet
1664
1665
        /**
1666
         * apply the set format on paste and displays it
1667
         *
1668
         * @name   PasteViewer.parsePaste
1669
         * @private
1670
         * @function
1671
         */
1672
        function parsePaste()
1673
        {
1674
            // skip parsing if no text is given
1675
            if (text === '') {
1676
                return;
1677
            }
1678
1679
            // escape HTML entities, link URLs, sanitize
1680
            var escapedLinkedText = Helper.urls2links(
1681
                    $('<div />').text(text).html()
1682
                ),
1683
                sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText);
1684
            $plainText.html(sanitizedLinkedText);
1685
            $prettyPrint.html(sanitizedLinkedText);
1686
1687
            switch (format) {
1688
                case 'markdown':
1689
                    var converter = new showdown.Converter({
1690
                        strikethrough: true,
1691
                        tables: true,
1692
                        tablesHeaderId: true
1693
                    });
1694
                    // let showdown convert the HTML and sanitize HTML *afterwards*!
1695
                    $plainText.html(
1696
                        DOMPurify.sanitize(converter.makeHtml(text))
1697
                    );
1698
                    // add table classes from bootstrap css
1699
                    $plainText.find('table').addClass('table-condensed table-bordered');
1700
                    break;
1701
                case 'syntaxhighlighting':
1702
                    // yes, this is really needed to initialize the environment
1703
                    if (typeof prettyPrint === 'function')
1704
                    {
1705
                        prettyPrint();
1706
                    }
1707
1708
                    $prettyPrint.html(
1709
                        DOMPurify.sanitize(
1710
                            prettyPrintOne(escapedLinkedText, null, true)
1711
                        )
1712
                    );
1713
                    // fall through, as the rest is the same
1714
                default: // = 'plaintext'
1715
                    $prettyPrint.css('white-space', 'pre-wrap');
1716
                    $prettyPrint.css('word-break', 'normal');
1717
                    $prettyPrint.removeClass('prettyprint');
1718
            }
1719
        }
1720
1721
        /**
1722
         * displays the paste
1723
         *
1724
         * @name   PasteViewer.showPaste
1725
         * @private
1726
         * @function
1727
         */
1728
        function showPaste()
1729
        {
1730
            // instead of "nothing" better display a placeholder
1731
            if (text === '') {
1732
                $placeholder.removeClass('hidden');
1733
                return;
1734
            }
1735
            // otherwise hide the placeholder
1736
            $placeholder.addClass('hidden');
1737
1738
            switch (format) {
1739
                case 'markdown':
1740
                    $plainText.removeClass('hidden');
1741
                    $prettyMessage.addClass('hidden');
1742
                    break;
1743
                default:
1744
                    $plainText.addClass('hidden');
1745
                    $prettyMessage.removeClass('hidden');
1746
                    break;
1747
            }
1748
        }
1749
1750
        /**
1751
         * sets the format in which the text is shown
1752
         *
1753
         * @name   PasteViewer.setFormat
1754
         * @function
1755
         * @param {string} newFormat the new format
1756
         */
1757
        me.setFormat = function(newFormat)
1758
        {
1759
            // skip if there is no update
1760
            if (format === newFormat) {
1761
                return;
1762
            }
1763
1764
            // needs to update display too, if we switch from or to Markdown
1765
            if (format === 'markdown' || newFormat === 'markdown') {
1766
                isDisplayed = false;
1767
            }
1768
1769
            format = newFormat;
1770
            isChanged = true;
1771
        };
1772
1773
        /**
1774
         * returns the current format
1775
         *
1776
         * @name   PasteViewer.getFormat
1777
         * @function
1778
         * @return {string}
1779
         */
1780
        me.getFormat = function()
1781
        {
1782
            return format;
1783
        };
1784
1785
        /**
1786
         * returns whether the current view is pretty printed
1787
         *
1788
         * @name   PasteViewer.isPrettyPrinted
1789
         * @function
1790
         * @return {bool}
1791
         */
1792
        me.isPrettyPrinted = function()
1793
        {
1794
            return $prettyPrint.hasClass('prettyprinted');
1795
        };
1796
1797
        /**
1798
         * sets the text to show
1799
         *
1800
         * @name   PasteViewer.setText
1801
         * @function
1802
         * @param {string} newText the text to show
1803
         */
1804
        me.setText = function(newText)
1805
        {
1806
            if (text !== newText) {
1807
                text = newText;
1808
                isChanged = true;
1809
            }
1810
        };
1811
1812
        /**
1813
         * gets the current cached text
1814
         *
1815
         * @name   PasteViewer.getText
1816
         * @function
1817
         * @return {string}
1818
         */
1819
        me.getText = function()
1820
        {
1821
            return text;
1822
        };
1823
1824
        /**
1825
         * show/update the parsed text (preview)
1826
         *
1827
         * @name   PasteViewer.run
1828
         * @function
1829
         */
1830
        me.run = function()
1831
        {
1832
            if (isChanged) {
1833
                parsePaste();
1834
                isChanged = false;
1835
            }
1836
1837
            if (!isDisplayed) {
1838
                showPaste();
1839
                isDisplayed = true;
1840
            }
1841
        };
1842
1843
        /**
1844
         * hide parsed text (preview)
1845
         *
1846
         * @name   PasteViewer.hide
1847
         * @function
1848
         */
1849
        me.hide = function()
1850
        {
1851
            if (!isDisplayed) {
1852
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1853
            }
1854
1855
            $plainText.addClass('hidden');
1856
            $prettyMessage.addClass('hidden');
1857
            $placeholder.addClass('hidden');
1858
1859
            isDisplayed = false;
1860
        };
1861
1862
        /**
1863
         * init status manager
1864
         *
1865
         * preloads jQuery elements
1866
         *
1867
         * @name   PasteViewer.init
1868
         * @function
1869
         */
1870
        me.init = function()
1871
        {
1872
            $placeholder = $('#placeholder');
1873
            $plainText = $('#plaintext');
1874
            $prettyMessage = $('#prettymessage');
1875
            $prettyPrint = $('#prettyprint');
1876
1877
            // check requirements
1878
            if (typeof prettyPrintOne !== 'function') {
1879
                Alert.showError([
1880
                    'The library %s is not available. This may cause display errors.',
1881
                    'pretty print'
1882
                ]);
1883
            }
1884
            if (typeof showdown !== 'object') {
1885
                Alert.showError([
1886
                    'The library %s is not available. This may cause display errors.',
1887
                    'showdown'
1888
                ]);
1889
            }
1890
1891
            // get default option from template/HTML or fall back to set value
1892
            format = Model.getFormatDefault() || format;
1893
            text = '';
1894
            isDisplayed = false;
1895
            isChanged = true;
1896
        };
1897
1898
        return me;
1899
    })();
1900
1901
    /**
1902
     * (view) Show attachment and preview if possible
1903
     *
1904
     * @name   AttachmentViewer
1905
     * @class
1906
     */
1907
    var AttachmentViewer = (function () {
1908
        var me = {};
1909
1910
        var $attachmentLink,
1911
            $attachmentPreview,
1912
            $attachment;
1913
1914
        var attachmentHasPreview = false;
1915
1916
        /**
1917
         * sets the attachment but does not yet show it
1918
         *
1919
         * @name   AttachmentViewer.setAttachment
1920
         * @function
1921
         * @param {string} attachmentData - base64-encoded data of file
1922
         * @param {string} fileName - optional, file name
1923
         */
1924
        me.setAttachment = function(attachmentData, fileName)
1925
        {
1926
            var imagePrefix = 'data:image/';
1927
1928
            // IE does not support setting a data URI on an a element
1929
            // Convert dataURI to a Blob and use msSaveBlob to download
1930
            if (window.Blob && navigator.msSaveBlob) {
1931
                $attachmentLink.off('click').on('click', function () {
1932
                    // data URI format: data:[<mediaType>][;base64],<data>
1933
1934
                    // position in data URI string of where data begins
1935
                    var base64Start = attachmentData.indexOf(',') + 1;
1936
                    // position in data URI string of where mediaType ends
1937
                    var mediaTypeEnd = attachmentData.indexOf(';');
1938
1939
                    // extract mediaType
1940
                    var mediaType = attachmentData.substring(5, mediaTypeEnd);
1941
                    // extract data and convert to binary
1942
                    var decodedData = Base64.atob(attachmentData.substring(base64Start));
1943
1944
                    // Transform into a Blob
1945
                    var decodedDataLength = decodedData.length;
1946
                    var buf = new Uint8Array(decodedDataLength);
1947
1948
                    for (var i = 0; i < decodedDataLength; i++) {
1949
                        buf[i] = decodedData.charCodeAt(i);
1950
                    }
1951
1952
                    var blob = new window.Blob([ buf ], { type: mediaType });
1953
                    navigator.msSaveBlob(blob, fileName);
1954
                });
1955
            } else {
1956
                $attachmentLink.attr('href', attachmentData);
1957
            }
1958
1959
            if (typeof fileName !== 'undefined') {
1960
                $attachmentLink.attr('download', fileName);
1961
            }
1962
1963
            // if the attachment is an image, display it
1964
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
1965
                $attachmentPreview.html(
1966
                    $(document.createElement('img'))
1967
                        .attr('src', attachmentData)
1968
                        .attr('class', 'img-thumbnail')
1969
                );
1970
                attachmentHasPreview = true;
1971
            }
1972
        };
1973
1974
        /**
1975
         * displays the attachment
1976
         *
1977
         * @name AttachmentViewer.showAttachment
1978
         * @function
1979
         */
1980
        me.showAttachment = function()
1981
        {
1982
            $attachment.removeClass('hidden');
1983
1984
            if (attachmentHasPreview) {
1985
                $attachmentPreview.removeClass('hidden');
1986
            }
1987
        };
1988
1989
        /**
1990
         * removes the attachment
1991
         *
1992
         * This automatically hides the attachment containers to, to
1993
         * prevent an inconsistent display.
1994
         *
1995
         * @name AttachmentViewer.removeAttachment
1996
         * @function
1997
         */
1998
        me.removeAttachment = function()
1999
        {
2000
            me.hideAttachment();
2001
            me.hideAttachmentPreview();
2002
            $attachmentLink.prop('href', '');
2003
            $attachmentLink.prop('download', '');
2004
            $attachmentLink.off('click');
2005
            $attachmentPreview.html('');
2006
        };
2007
2008
        /**
2009
         * hides the attachment
2010
         *
2011
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2012
         * for that) nor will it hide the attachment link if it was moved somewhere
2013
         * else (see AttachmentViewer.moveAttachmentTo).
2014
         *
2015
         * @name AttachmentViewer.hideAttachment
2016
         * @function
2017
         */
2018
        me.hideAttachment = function()
2019
        {
2020
            $attachment.addClass('hidden');
2021
        };
2022
2023
        /**
2024
         * hides the attachment preview
2025
         *
2026
         * @name AttachmentViewer.hideAttachmentPreview
2027
         * @function
2028
         */
2029
        me.hideAttachmentPreview = function()
2030
        {
2031
            $attachmentPreview.addClass('hidden');
2032
        };
2033
2034
        /**
2035
         * checks if there is an attachment
2036
         *
2037
         * @name   AttachmentViewer.hasAttachment
2038
         * @function
2039
         */
2040
        me.hasAttachment = function()
2041
        {
2042
            var link = $attachmentLink.prop('href');
2043
            return typeof link !== 'undefined' && link !== '';
2044
        };
2045
2046
        /**
2047
         * return the attachment
2048
         *
2049
         * @name   AttachmentViewer.getAttachment
2050
         * @function
2051
         * @returns {array}
2052
         */
2053
        me.getAttachment = function()
2054
        {
2055
            return [
2056
                $attachmentLink.prop('href'),
2057
                $attachmentLink.prop('download')
2058
            ];
2059
        };
2060
2061
        /**
2062
         * moves the attachment link to another element
2063
         *
2064
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2065
         *
2066
         * @name   AttachmentViewer.moveAttachmentTo
2067
         * @function
2068
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2069
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2070
         */
2071
        me.moveAttachmentTo = function($element, label)
2072
        {
2073
            // move elemement to new place
2074
            $attachmentLink.appendTo($element);
2075
2076
            // update text
2077
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2078
        };
2079
2080
        /**
2081
         * initiate
2082
         *
2083
         * preloads jQuery elements
2084
         *
2085
         * @name   AttachmentViewer.init
2086
         * @function
2087
         */
2088
        me.init = function()
2089
        {
2090
            $attachment = $('#attachment');
2091
            $attachmentLink = $('#attachment a');
2092
            $attachmentPreview = $('#attachmentPreview');
2093
            attachmentHasPreview = false;
2094
        };
2095
2096
        return me;
2097
    })();
2098
2099
    /**
2100
     * (view) Shows discussion thread and handles replies
2101
     *
2102
     * @name   DiscussionViewer
2103
     * @class
2104
     */
2105
    var DiscussionViewer = (function () {
2106
        var me = {};
2107
2108
        var $commentTail,
2109
            $discussion,
2110
            $reply,
2111
            $replyMessage,
2112
            $replyNickname,
2113
            $replyStatus,
2114
            $commentContainer;
2115
2116
        var replyCommentId;
2117
2118
        /**
2119
         * initializes the templates
2120
         *
2121
         * @name   DiscussionViewer.initTemplates
2122
         * @private
2123
         * @function
2124
         */
2125
        function initTemplates()
2126
        {
2127
            $reply = Model.getTemplate('reply');
2128
            $replyMessage = $reply.find('#replymessage');
2129
            $replyNickname = $reply.find('#nickname');
2130
            $replyStatus = $reply.find('#replystatus');
2131
2132
            // cache jQuery elements
2133
            $commentTail = Model.getTemplate('commenttail');
2134
        }
2135
2136
        /**
2137
         * open the comment entry when clicking the "Reply" button of a comment
2138
         *
2139
         * @name   DiscussionViewer.openReply
2140
         * @private
2141
         * @function
2142
         * @param  {Event} event
2143
         */
2144
        function openReply(event)
2145
        {
2146
            var $source = $(event.target);
2147
2148
            // clear input
2149
            $replyMessage.val('');
2150
            $replyNickname.val('');
2151
2152
            // get comment id from source element
2153
            replyCommentId = $source.parent().prop('id').split('_')[1];
2154
2155
            // move to correct position
2156
            $source.after($reply);
2157
2158
            // show
2159
            $reply.removeClass('hidden');
2160
            $replyMessage.focus();
2161
2162
            event.preventDefault();
2163
        }
2164
2165
        /**
2166
         * custom handler for displaying notifications in own status message area
2167
         *
2168
         * @name   DiscussionViewer.handleNotification
2169
         * @function
2170
         * @param  {string} alertType
2171
         * @return {bool|jQuery}
2172
         */
2173
        me.handleNotification = function(alertType)
2174
        {
2175
            // ignore loading messages
2176
            if (alertType === 'loading') {
2177
                return false;
2178
            }
2179
2180
            if (alertType === 'danger') {
2181
                $replyStatus.removeClass('alert-info');
2182
                $replyStatus.addClass('alert-danger');
2183
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2184
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2185
            } else {
2186
                $replyStatus.removeClass('alert-danger');
2187
                $replyStatus.addClass('alert-info');
2188
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2189
                $replyStatus.find(':first').addClass('glyphicon-alert');
2190
            }
2191
2192
            return $replyStatus;
2193
        };
2194
2195
        /**
2196
         * adds another comment
2197
         *
2198
         * @name   DiscussionViewer.addComment
2199
         * @function
2200
         * @param {object} comment
2201
         * @param {string} commentText
2202
         * @param {string} nickname
2203
         */
2204
        me.addComment = function(comment, commentText, nickname)
2205
        {
2206
            if (commentText === '') {
2207
                commentText = 'comment decryption failed';
2208
            }
2209
2210
            // create new comment based on template
2211
            var $commentEntry = Model.getTemplate('comment');
2212
            $commentEntry.prop('id', 'comment_' + comment.id);
2213
            var $commentEntryData = $commentEntry.find('div.commentdata');
2214
2215
            // set & parse text
2216
            $commentEntryData.html(
2217
                DOMPurify.sanitize(
2218
                    Helper.urls2links(commentText)
2219
                )
2220
            );
2221
2222
            // set nickname
2223
            if (nickname.length > 0) {
2224
                $commentEntry.find('span.nickname').text(nickname);
2225
            } else {
2226
                $commentEntry.find('span.nickname').html('<i></i>');
2227
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2228
            }
2229
2230
            // set date
2231
            $commentEntry.find('span.commentdate')
2232
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2233
                      .attr('title', 'CommentID: ' + comment.id);
2234
2235
            // if an avatar is available, display it
2236
            if (comment.meta.vizhash) {
2237
                $commentEntry.find('span.nickname')
2238
                             .before(
2239
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2240
                             );
2241
                $(document).on('languageLoaded', function () {
2242
                    $commentEntry.find('img.vizhash')
2243
                                 .prop('title', I18n._('Avatar generated from IP address'));
2244
                });
2245
            }
2246
2247
            // starting point (default value/fallback)
2248
            var $place = $commentContainer;
2249
2250
            // if parent comment exists
2251
            var $parentComment = $('#comment_' + comment.parentid);
2252
            if ($parentComment.length) {
2253
                // use parent as position for new comment, so it is shifted
2254
                // to the right
2255
                $place = $parentComment;
2256
            }
2257
2258
            // finally append comment
2259
            $place.append($commentEntry);
2260
        };
2261
2262
        /**
2263
         * finishes the discussion area after last comment
2264
         *
2265
         * @name   DiscussionViewer.finishDiscussion
2266
         * @function
2267
         */
2268
        me.finishDiscussion = function()
2269
        {
2270
            // add 'add new comment' area
2271
            $commentContainer.append($commentTail);
2272
2273
            // show discussions
2274
            $discussion.removeClass('hidden');
2275
        };
2276
2277
        /**
2278
         * removes the old discussion and prepares everything for creating a new
2279
         * one.
2280
         *
2281
         * @name   DiscussionViewer.prepareNewDiscussion
2282
         * @function
2283
         */
2284
        me.prepareNewDiscussion = function()
2285
        {
2286
            $commentContainer.html('');
2287
            $discussion.addClass('hidden');
2288
2289
            // (re-)init templates
2290
            initTemplates();
2291
        };
2292
2293
        /**
2294
         * returns the users message from the reply form
2295
         *
2296
         * @name   DiscussionViewer.getReplyMessage
2297
         * @function
2298
         * @return {String}
2299
         */
2300
        me.getReplyMessage = function()
2301
        {
2302
            return $replyMessage.val();
2303
        };
2304
2305
        /**
2306
         * returns the users nickname (if any) from the reply form
2307
         *
2308
         * @name   DiscussionViewer.getReplyNickname
2309
         * @function
2310
         * @return {String}
2311
         */
2312
        me.getReplyNickname = function()
2313
        {
2314
            return $replyNickname.val();
2315
        };
2316
2317
        /**
2318
         * returns the id of the parent comment the user is replying to
2319
         *
2320
         * @name   DiscussionViewer.getReplyCommentId
2321
         * @function
2322
         * @return {int|undefined}
2323
         */
2324
        me.getReplyCommentId = function()
2325
        {
2326
            return replyCommentId;
2327
        };
2328
2329
        /**
2330
         * highlights a specific comment and scrolls to it if necessary
2331
         *
2332
         * @name   DiscussionViewer.highlightComment
2333
         * @function
2334
         * @param {string} commentId
2335
         * @param {bool} fadeOut - whether to fade out the comment
2336
         */
2337
        me.highlightComment = function(commentId, fadeOut)
2338
        {
2339
            var $comment = $('#comment_' + commentId);
2340
            // in case comment does not exist, cancel
2341
            if ($comment.length === 0) {
2342
                return;
2343
            }
2344
2345
            var highlightComment = function () {
2346
                $comment.addClass('highlight');
2347
                if (fadeOut === true) {
2348
                    setTimeout(function () {
2349
                        $comment.removeClass('highlight');
2350
                    }, 300);
2351
                }
2352
            };
2353
2354
            if (UiHelper.isVisible($comment)) {
2355
                return highlightComment();
2356
            }
2357
2358
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2359
        };
2360
2361
        /**
2362
         * initiate
2363
         *
2364
         * preloads jQuery elements
2365
         *
2366
         * @name   DiscussionViewer.init
2367
         * @function
2368
         */
2369
        me.init = function()
2370
        {
2371
            // bind events to templates (so they are later cloned)
2372
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2373
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2374
2375
            $commentContainer = $('#commentcontainer');
2376
            $discussion = $('#discussion');
2377
        };
2378
2379
        return me;
2380
    })();
2381
2382
    /**
2383
     * Manage top (navigation) bar
2384
     *
2385
     * @name   TopNav
2386
     * @param  {object} window
2387
     * @param  {object} document
2388
     * @class
2389
     */
2390
    var TopNav = (function (window, document) {
2391
        var me = {};
2392
2393
        var createButtonsDisplayed = false;
2394
        var viewButtonsDisplayed = false;
2395
2396
        var $attach,
2397
            $burnAfterReading,
2398
            $burnAfterReadingOption,
2399
            $cloneButton,
2400
            $customAttachment,
2401
            $expiration,
2402
            $fileRemoveButton,
2403
            $fileWrap,
2404
            $formatter,
2405
            $newButton,
2406
            $openDiscussion,
2407
            $openDiscussionOption,
2408
            $password,
2409
            $passwordInput,
2410
            $rawTextButton,
2411
            $qrCodeLink,
2412
            $sendButton;
2413
2414
        var pasteExpiration = '1week';
2415
2416
        /**
2417
         * set the expiration on bootstrap templates in dropdown
2418
         *
2419
         * @name   TopNav.updateExpiration
2420
         * @private
2421
         * @function
2422
         * @param  {Event} event
2423
         */
2424
        function updateExpiration(event)
2425
        {
2426
            // get selected option
2427
            var target = $(event.target);
2428
2429
            // update dropdown display and save new expiration time
2430
            $('#pasteExpirationDisplay').text(target.text());
2431
            pasteExpiration = target.data('expiration');
2432
2433
            event.preventDefault();
2434
        }
2435
2436
        /**
2437
         * set the format on bootstrap templates in dropdown
2438
         *
2439
         * @name   TopNav.updateFormat
2440
         * @private
2441
         * @function
2442
         * @param  {Event} event
2443
         */
2444
        function updateFormat(event)
2445
        {
2446
            // get selected option
2447
            var $target = $(event.target);
2448
2449
            // update dropdown display and save new format
2450
            var newFormat = $target.data('format');
2451
            $('#pasteFormatterDisplay').text($target.text());
2452
            PasteViewer.setFormat(newFormat);
2453
2454
            // update preview
2455
            if (Editor.isPreview()) {
2456
                PasteViewer.run();
2457
            }
2458
2459
            event.preventDefault();
2460
        }
2461
2462
        /**
2463
         * when "burn after reading" is checked, disable discussion
2464
         *
2465
         * @name   TopNav.changeBurnAfterReading
2466
         * @private
2467
         * @function
2468
         */
2469
        function changeBurnAfterReading()
2470
        {
2471
            if ($burnAfterReading.is(':checked')) {
2472
                $openDiscussionOption.addClass('buttondisabled');
2473
                $openDiscussion.prop('checked', false);
2474
2475
                // if button is actually disabled, force-enable it and uncheck other button
2476
                $burnAfterReadingOption.removeClass('buttondisabled');
2477
            } else {
2478
                $openDiscussionOption.removeClass('buttondisabled');
2479
            }
2480
        }
2481
2482
        /**
2483
         * when discussion is checked, disable "burn after reading"
2484
         *
2485
         * @name   TopNav.changeOpenDiscussion
2486
         * @private
2487
         * @function
2488
         */
2489
        function changeOpenDiscussion()
2490
        {
2491
            if ($openDiscussion.is(':checked')) {
2492
                $burnAfterReadingOption.addClass('buttondisabled');
2493
                $burnAfterReading.prop('checked', false);
2494
2495
                // if button is actually disabled, force-enable it and uncheck other button
2496
                $openDiscussionOption.removeClass('buttondisabled');
2497
            } else {
2498
                $burnAfterReadingOption.removeClass('buttondisabled');
2499
            }
2500
        }
2501
2502
        /**
2503
         * return raw text
2504
         *
2505
         * @name   TopNav.rawText
2506
         * @private
2507
         * @function
2508
         */
2509
        function rawText()
2510
        {
2511
            TopNav.hideAllButtons();
2512
            Alert.showLoading('Showing raw text…', 'time');
2513
            var paste = PasteViewer.getText();
2514
2515
            // push a new state to allow back navigation with browser back button
2516
            history.pushState(
2517
                {type: 'raw'},
2518
                document.title,
2519
                // recreate paste URL
2520
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2521
                Model.getPasteKey()
2522
            );
2523
2524
            // we use text/html instead of text/plain to avoid a bug when
2525
            // reloading the raw text view (it reverts to type text/html)
2526
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2527
            var newDoc = document.open('text/html', 'replace');
2528
            newDoc.write('<!DOCTYPE html><html><head>');
2529
            for (var i = 0; i < $head.length; i++) {
2530
                newDoc.write($head[i].outerHTML);
2531
            }
2532
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2533
            newDoc.close();
2534
        }
2535
2536
        /**
2537
         * saves the language in a cookie and reloads the page
2538
         *
2539
         * @name   TopNav.setLanguage
2540
         * @private
2541
         * @function
2542
         * @param  {Event} event
2543
         */
2544
        function setLanguage(event)
2545
        {
2546
            document.cookie = 'lang=' + $(event.target).data('lang');
2547
            UiHelper.reloadHome();
2548
        }
2549
2550
        /**
2551
         * hides all messages and creates a new paste
2552
         *
2553
         * @name   TopNav.clickNewPaste
2554
         * @private
2555
         * @function
2556
         */
2557
        function clickNewPaste()
2558
        {
2559
            Controller.hideStatusMessages();
2560
            Controller.newPaste();
2561
        }
2562
2563
        /**
2564
         * removes the existing attachment
2565
         *
2566
         * @name   TopNav.removeAttachment
2567
         * @private
2568
         * @function
2569
         * @param  {Event} event
2570
         */
2571
        function removeAttachment(event)
2572
        {
2573
            // if custom attachment is used, remove it first
2574
            if (!$customAttachment.hasClass('hidden')) {
2575
                AttachmentViewer.removeAttachment();
2576
                $customAttachment.addClass('hidden');
2577
                $fileWrap.removeClass('hidden');
2578
            }
2579
2580
            // our up-to-date jQuery can handle it :)
2581
            $fileWrap.find('input').val('');
2582
2583
            // pevent '#' from appearing in the URL
2584
            event.preventDefault();
2585
        }
2586
2587
        /**
2588
         * Shows the QR code of the current paste (URL).
2589
         *
2590
         * @name   TopNav.displayQrCode
2591
         * @private
2592
         * @function
2593
         */
2594
        function displayQrCode()
2595
        {
2596
            var qrCanvas = kjua({
2597
                render: 'canvas',
2598
                text: window.location.href
2599
            });
2600
            $('#qrcode-display').html(qrCanvas);
2601
        }
2602
2603
        /**
2604
         * Shows all navigation elements for viewing an existing paste
2605
         *
2606
         * @name   TopNav.showViewButtons
2607
         * @function
2608
         */
2609
        me.showViewButtons = function()
2610
        {
2611
            if (viewButtonsDisplayed) {
2612
                console.warn('showViewButtons: view buttons are already displayed');
2613
                return;
2614
            }
2615
2616
            $newButton.removeClass('hidden');
2617
            $cloneButton.removeClass('hidden');
2618
            $rawTextButton.removeClass('hidden');
2619
            $qrCodeLink.removeClass('hidden');
2620
2621
            viewButtonsDisplayed = true;
2622
        };
2623
2624
        /**
2625
         * Hides all navigation elements for viewing an existing paste
2626
         *
2627
         * @name   TopNav.hideViewButtons
2628
         * @function
2629
         */
2630
        me.hideViewButtons = function()
2631
        {
2632
            if (!viewButtonsDisplayed) {
2633
                console.warn('hideViewButtons: view buttons are already hidden');
2634
                return;
2635
            }
2636
2637
            $newButton.addClass('hidden');
2638
            $cloneButton.addClass('hidden');
2639
            $rawTextButton.addClass('hidden');
2640
            $qrCodeLink.addClass('hidden');
2641
2642
            viewButtonsDisplayed = false;
2643
        };
2644
2645
        /**
2646
         * Hides all elements belonging to existing pastes
2647
         *
2648
         * @name   TopNav.hideAllButtons
2649
         * @function
2650
         */
2651
        me.hideAllButtons = function()
2652
        {
2653
            me.hideViewButtons();
2654
            me.hideCreateButtons();
2655
        };
2656
2657
        /**
2658
         * shows all elements needed when creating a new paste
2659
         *
2660
         * @name   TopNav.showCreateButtons
2661
         * @function
2662
         */
2663
        me.showCreateButtons = function()
2664
        {
2665
            if (createButtonsDisplayed) {
2666
                console.warn('showCreateButtons: create buttons are already displayed');
2667
                return;
2668
            }
2669
2670
            $sendButton.removeClass('hidden');
2671
            $expiration.removeClass('hidden');
2672
            $formatter.removeClass('hidden');
2673
            $burnAfterReadingOption.removeClass('hidden');
2674
            $openDiscussionOption.removeClass('hidden');
2675
            $newButton.removeClass('hidden');
2676
            $password.removeClass('hidden');
2677
            $attach.removeClass('hidden');
2678
2679
            createButtonsDisplayed = true;
2680
        };
2681
2682
        /**
2683
         * shows all elements needed when creating a new paste
2684
         *
2685
         * @name   TopNav.hideCreateButtons
2686
         * @function
2687
         */
2688
        me.hideCreateButtons = function()
2689
        {
2690
            if (!createButtonsDisplayed) {
2691
                console.warn('hideCreateButtons: create buttons are already hidden');
2692
                return;
2693
            }
2694
2695
            $newButton.addClass('hidden');
2696
            $sendButton.addClass('hidden');
2697
            $expiration.addClass('hidden');
2698
            $formatter.addClass('hidden');
2699
            $burnAfterReadingOption.addClass('hidden');
2700
            $openDiscussionOption.addClass('hidden');
2701
            $password.addClass('hidden');
2702
            $attach.addClass('hidden');
2703
2704
            createButtonsDisplayed = false;
2705
        };
2706
2707
        /**
2708
         * only shows the "new paste" button
2709
         *
2710
         * @name   TopNav.showNewPasteButton
2711
         * @function
2712
         */
2713
        me.showNewPasteButton = function()
2714
        {
2715
            $newButton.removeClass('hidden');
2716
        };
2717
2718
        /**
2719
         * only hides the clone button
2720
         *
2721
         * @name   TopNav.hideCloneButton
2722
         * @function
2723
         */
2724
        me.hideCloneButton = function()
2725
        {
2726
            $cloneButton.addClass('hidden');
2727
        };
2728
2729
        /**
2730
         * only hides the raw text button
2731
         *
2732
         * @name   TopNav.hideRawButton
2733
         * @function
2734
         */
2735
        me.hideRawButton = function()
2736
        {
2737
            $rawTextButton.addClass('hidden');
2738
        };
2739
2740
        /**
2741
         * hides the file selector in attachment
2742
         *
2743
         * @name   TopNav.hideFileSelector
2744
         * @function
2745
         */
2746
        me.hideFileSelector = function()
2747
        {
2748
            $fileWrap.addClass('hidden');
2749
        };
2750
2751
2752
        /**
2753
         * shows the custom attachment
2754
         *
2755
         * @name   TopNav.showCustomAttachment
2756
         * @function
2757
         */
2758
        me.showCustomAttachment = function()
2759
        {
2760
            $customAttachment.removeClass('hidden');
2761
        };
2762
2763
        /**
2764
         * collapses the navigation bar, only if expanded
2765
         *
2766
         * @name   TopNav.collapseBar
2767
         * @function
2768
         */
2769
        me.collapseBar = function()
2770
        {
2771
            if ($('#navbar').attr('aria-expanded') === 'true') {
2772
                $('.navbar-toggle').click();
2773
            }
2774
        };
2775
2776
        /**
2777
         * returns the currently set expiration time
2778
         *
2779
         * @name   TopNav.getExpiration
2780
         * @function
2781
         * @return {int}
2782
         */
2783
        me.getExpiration = function()
2784
        {
2785
            return pasteExpiration;
2786
        };
2787
2788
        /**
2789
         * returns the currently selected file(s)
2790
         *
2791
         * @name   TopNav.getFileList
2792
         * @function
2793
         * @return {FileList|null}
2794
         */
2795
        me.getFileList = function()
2796
        {
2797
            var $file = $('#file');
2798
2799
            // if no file given, return null
2800
            if (!$file.length || !$file[0].files.length) {
2801
                return null;
2802
            }
2803
2804
            // ensure the selected file is still accessible
2805
            if (!($file[0].files && $file[0].files[0])) {
2806
                return null;
2807
            }
2808
2809
            return $file[0].files;
2810
        };
2811
2812
        /**
2813
         * returns the state of the burn after reading checkbox
2814
         *
2815
         * @name   TopNav.getExpiration
2816
         * @function
2817
         * @return {bool}
2818
         */
2819
        me.getBurnAfterReading = function()
2820
        {
2821
            return $burnAfterReading.is(':checked');
2822
        };
2823
2824
        /**
2825
         * returns the state of the discussion checkbox
2826
         *
2827
         * @name   TopNav.getOpenDiscussion
2828
         * @function
2829
         * @return {bool}
2830
         */
2831
        me.getOpenDiscussion = function()
2832
        {
2833
            return $openDiscussion.is(':checked');
2834
        };
2835
2836
        /**
2837
         * returns the entered password
2838
         *
2839
         * @name   TopNav.getPassword
2840
         * @function
2841
         * @return {string}
2842
         */
2843
        me.getPassword = function()
2844
        {
2845
            return $passwordInput.val();
2846
        };
2847
2848
        /**
2849
         * returns the element where custom attachments can be placed
2850
         *
2851
         * Used by AttachmentViewer when an attachment is cloned here.
2852
         *
2853
         * @name   TopNav.getCustomAttachment
2854
         * @function
2855
         * @return {jQuery}
2856
         */
2857
        me.getCustomAttachment = function()
2858
        {
2859
            return $customAttachment;
2860
        };
2861
2862
        /**
2863
         * init navigation manager
2864
         *
2865
         * preloads jQuery elements
2866
         *
2867
         * @name   TopNav.init
2868
         * @function
2869
         */
2870
        me.init = function()
2871
        {
2872
            $attach = $('#attach');
2873
            $burnAfterReading = $('#burnafterreading');
2874
            $burnAfterReadingOption = $('#burnafterreadingoption');
2875
            $cloneButton = $('#clonebutton');
2876
            $customAttachment = $('#customattachment');
2877
            $expiration = $('#expiration');
2878
            $fileRemoveButton = $('#fileremovebutton');
2879
            $fileWrap = $('#filewrap');
2880
            $formatter = $('#formatter');
2881
            $newButton = $('#newbutton');
2882
            $openDiscussion = $('#opendiscussion');
2883
            $openDiscussionOption = $('#opendiscussionoption');
2884
            $password = $('#password');
2885
            $passwordInput = $('#passwordinput');
2886
            $rawTextButton = $('#rawtextbutton');
2887
            $sendButton = $('#sendbutton');
2888
            $qrCodeLink = $('#qrcodelink');
2889
2890
            // bootstrap template drop down
2891
            $('#language ul.dropdown-menu li a').click(setLanguage);
2892
            // page template drop down
2893
            $('#language select option').click(setLanguage);
2894
2895
            // bind events
2896
            $burnAfterReading.change(changeBurnAfterReading);
2897
            $openDiscussionOption.change(changeOpenDiscussion);
2898
            $newButton.click(clickNewPaste);
2899
            $sendButton.click(PasteEncrypter.sendPaste);
2900
            $cloneButton.click(Controller.clonePaste);
2901
            $rawTextButton.click(rawText);
2902
            $fileRemoveButton.click(removeAttachment);
2903
            $qrCodeLink.click(displayQrCode);
2904
2905
            // bootstrap template drop downs
2906
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2907
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2908
2909
            // initiate default state of checkboxes
2910
            changeBurnAfterReading();
2911
            changeOpenDiscussion();
2912
2913
            // get default value from template or fall back to set value
2914
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2915
2916
            createButtonsDisplayed = false;
2917
            viewButtonsDisplayed = false;
2918
        };
2919
2920
        return me;
2921
    })(window, document);
2922
2923
    /**
2924
     * Responsible for AJAX requests, transparently handles encryption…
2925
     *
2926
     * @name   Uploader
2927
     * @class
2928
     */
2929
    var Uploader = (function () {
2930
        var me = {};
2931
2932
        var successFunc = null,
2933
            failureFunc = null,
2934
            url,
2935
            data,
2936
            symmetricKey,
2937
            password;
2938
2939
        /**
2940
         * public variable ('constant') for errors to prevent magic numbers
2941
         *
2942
         * @name   Uploader.error
2943
         * @readonly
2944
         * @enum   {Object}
2945
         */
2946
        me.error = {
2947
            okay: 0,
2948
            custom: 1,
2949
            unknown: 2,
2950
            serverError: 3
2951
        };
2952
2953
        /**
2954
         * ajaxHeaders to send in AJAX requests
2955
         *
2956
         * @name   Uploader.ajaxHeaders
2957
         * @private
2958
         * @readonly
2959
         * @enum   {Object}
2960
         */
2961
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2962
2963
        /**
2964
         * called after successful upload
2965
         *
2966
         * @name   Uploader.checkCryptParameters
2967
         * @private
2968
         * @function
2969
         * @throws {string}
2970
         */
2971
        function checkCryptParameters()
2972
        {
2973
            // workaround for this nasty 'bug' in ECMAScript
2974
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
2975
            var typeOfKey = typeof symmetricKey;
2976
            if (symmetricKey === null) {
2977
                typeOfKey = 'null';
2978
            }
2979
2980
            // in case of missing preparation, throw error
2981
            switch (typeOfKey) {
2982
                case 'string':
2983
                    // already set, all right
2984
                    return;
2985
                case 'null':
2986
                    // needs to be generated auto-generate
2987
                    symmetricKey = CryptTool.getSymmetricKey();
2988
                    break;
2989
                default:
2990
                    console.error('current invalid symmetricKey:', symmetricKey);
2991
                    throw 'symmetricKey is invalid, probably the module was not prepared';
2992
            }
2993
            // password is optional
2994
        }
2995
2996
        /**
2997
         * called after successful upload
2998
         *
2999
         * @name   Uploader.success
3000
         * @private
3001
         * @function
3002
         * @param {int} status
3003
         * @param {int} result - optional
3004
         */
3005
        function success(status, result)
3006
        {
3007
            // add useful data to result
3008
            result.encryptionKey = symmetricKey;
3009
            result.requestData = data;
3010
3011
            if (successFunc !== null) {
3012
                successFunc(status, result);
3013
            }
3014
        }
3015
3016
        /**
3017
         * called after a upload failure
3018
         *
3019
         * @name   Uploader.fail
3020
         * @private
3021
         * @function
3022
         * @param {int} status - internal code
3023
         * @param {int} result - original error code
3024
         */
3025
        function fail(status, result)
3026
        {
3027
            if (failureFunc !== null) {
3028
                failureFunc(status, result);
3029
            }
3030
        }
3031
3032
        /**
3033
         * actually uploads the data
3034
         *
3035
         * @name   Uploader.run
3036
         * @function
3037
         */
3038
        me.run = function()
3039
        {
3040
            $.ajax({
3041
                type: 'POST',
3042
                url: url,
3043
                data: data,
3044
                dataType: 'json',
3045
                headers: ajaxHeaders,
3046
                success: function(result) {
3047
                    if (result.status === 0) {
3048
                        success(0, result);
3049
                    } else if (result.status === 1) {
3050
                        fail(1, result);
3051
                    } else {
3052
                        fail(2, result);
3053
                    }
3054
                }
3055
            })
3056
            .fail(function(jqXHR, textStatus, errorThrown) {
3057
                console.error(textStatus, errorThrown);
3058
                fail(3, jqXHR);
3059
            });
3060
        };
3061
3062
        /**
3063
         * set success function
3064
         *
3065
         * @name   Uploader.setUrl
3066
         * @function
3067
         * @param {function} newUrl
3068
         */
3069
        me.setUrl = function(newUrl)
3070
        {
3071
            url = newUrl;
3072
        };
3073
3074
        /**
3075
         * sets the password to use (first value) and optionally also the
3076
         * encryption key (not recommend, it is automatically generated).
3077
         *
3078
         * Note: Call this after prepare() as prepare() resets these values.
3079
         *
3080
         * @name   Uploader.setCryptValues
3081
         * @function
3082
         * @param {string} newPassword
3083
         * @param {string} newKey       - optional
3084
         */
3085
        me.setCryptParameters = function(newPassword, newKey)
3086
        {
3087
            password = newPassword;
3088
3089
            if (typeof newKey !== 'undefined') {
3090
                symmetricKey = newKey;
3091
            }
3092
        };
3093
3094
        /**
3095
         * set success function
3096
         *
3097
         * @name   Uploader.setSuccess
3098
         * @function
3099
         * @param {function} func
3100
         */
3101
        me.setSuccess = function(func)
3102
        {
3103
            successFunc = func;
3104
        };
3105
3106
        /**
3107
         * set failure function
3108
         *
3109
         * @name   Uploader.setFailure
3110
         * @function
3111
         * @param {function} func
3112
         */
3113
        me.setFailure = function(func)
3114
        {
3115
            failureFunc = func;
3116
        };
3117
3118
        /**
3119
         * prepares a new upload
3120
         *
3121
         * Call this when doing a new upload to reset any data from potential
3122
         * previous uploads. Must be called before any other method of this
3123
         * module.
3124
         *
3125
         * @name   Uploader.prepare
3126
         * @function
3127
         * @return {object}
3128
         */
3129
        me.prepare = function()
3130
        {
3131
            // entropy should already be checked!
3132
3133
            // reset password
3134
            password = '';
3135
3136
            // reset key, so it a new one is generated when it is used
3137
            symmetricKey = null;
3138
3139
            // reset data
3140
            successFunc = null;
3141
            failureFunc = null;
3142
            url = Helper.baseUri();
3143
            data = {};
3144
        };
3145
3146
        /**
3147
         * encrypts and sets the data
3148
         *
3149
         * @name   Uploader.setData
3150
         * @function
3151
         * @param {string} index
3152
         * @param {mixed} element
3153
         */
3154
        me.setData = function(index, element)
3155
        {
3156
            checkCryptParameters();
3157
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3158
        };
3159
3160
        /**
3161
         * set the additional metadata to send unencrypted
3162
         *
3163
         * @name   Uploader.setUnencryptedData
3164
         * @function
3165
         * @param {string} index
3166
         * @param {mixed} element
3167
         */
3168
        me.setUnencryptedData = function(index, element)
3169
        {
3170
            data[index] = element;
3171
        };
3172
3173
        /**
3174
         * set the additional metadata to send unencrypted passed at once
3175
         *
3176
         * @name   Uploader.setUnencryptedData
3177
         * @function
3178
         * @param {object} newData
3179
         */
3180
        me.setUnencryptedBulkData = function(newData)
3181
        {
3182
            $.extend(data, newData);
3183
        };
3184
3185
        /**
3186
         * Helper, which parses shows a general error message based on the result of the Uploader
3187
         *
3188
         * @name    Uploader.parseUploadError
3189
         * @function
3190
         * @param {int} status
3191
         * @param {object} data
3192
         * @param {string} doThisThing - a human description of the action, which was tried
3193
         * @return {array}
3194
         */
3195
        me.parseUploadError = function(status, data, doThisThing) {
3196
            var errorArray;
3197
3198
            switch (status) {
3199
                case me.error.custom:
3200
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3201
                    break;
3202
                case me.error.unknown:
3203
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3204
                    break;
3205
                case me.error.serverError:
3206
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3207
                    break;
3208
                default:
3209
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3210
                    break;
3211
            }
3212
3213
            return errorArray;
3214
        };
3215
3216
        /**
3217
         * init Uploader
3218
         *
3219
         * @name   Uploader.init
3220
         * @function
3221
         */
3222
        me.init = function()
3223
        {
3224
            // nothing yet
3225
        };
3226
3227
        return me;
3228
    })();
3229
3230
    /**
3231
     * (controller) Responsible for encrypting paste and sending it to server.
3232
     *
3233
     * Does upload, encryption is done transparently by Uploader.
3234
     *
3235
     * @name PasteEncrypter
3236
     * @class
3237
     */
3238
    var PasteEncrypter = (function () {
3239
        var me = {};
3240
3241
        var requirementsChecked = false;
3242
3243
        /**
3244
         * checks whether there is a suitable amount of entrophy
3245
         *
3246
         * @name PasteEncrypter.checkRequirements
3247
         * @private
3248
         * @function
3249
         * @param {function} retryCallback - the callback to execute to retry the upload
3250
         * @return {bool}
3251
         */
3252
        function checkRequirements(retryCallback) {
3253
            // skip double requirement checks
3254
            if (requirementsChecked === true) {
3255
                return true;
3256
            }
3257
3258
            if (!CryptTool.isEntropyReady()) {
3259
                // display a message and wait
3260
                Alert.showStatus('Please move your mouse for more entropy…');
3261
3262
                CryptTool.addEntropySeedListener(retryCallback);
3263
                return false;
3264
            }
3265
3266
            requirementsChecked = true;
3267
3268
            return true;
3269
        }
3270
3271
        /**
3272
         * called after successful paste upload
3273
         *
3274
         * @name PasteEncrypter.showCreatedPaste
3275
         * @private
3276
         * @function
3277
         * @param {int} status
3278
         * @param {object} data
3279
         */
3280
        function showCreatedPaste(status, data) {
3281
            Alert.hideLoading();
3282
3283
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3284
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3285
3286
            Alert.hideMessages();
3287
3288
            // show notification
3289
            PasteStatus.createPasteNotification(url, deleteUrl);
3290
3291
            // show new URL in browser bar
3292
            history.pushState({type: 'newpaste'}, document.title, url);
3293
3294
            TopNav.showViewButtons();
3295
            TopNav.hideRawButton();
3296
            Editor.hide();
3297
3298
            // parse and show text
3299
            // (preparation already done in me.sendPaste())
3300
            PasteViewer.run();
3301
        }
3302
3303
        /**
3304
         * called after successful comment upload
3305
         *
3306
         * @name PasteEncrypter.showUploadedComment
3307
         * @private
3308
         * @function
3309
         * @param {int} status
3310
         * @param {object} data
3311
         */
3312
        function showUploadedComment(status, data) {
3313
            // show success message
3314
            Alert.showStatus('Comment posted.');
3315
3316
            // reload paste
3317
            Controller.refreshPaste(function () {
3318
                // highlight sent comment
3319
                DiscussionViewer.highlightComment(data.id, true);
3320
                // reset error handler
3321
                Alert.setCustomHandler(null);
3322
            });
3323
        }
3324
3325
        /**
3326
         * adds attachments to the Uploader
3327
         *
3328
         * @name PasteEncrypter.encryptAttachments
3329
         * @private
3330
         * @function
3331
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3332
         * @param {function} callback - excuted when action is successful
3333
         */
3334
        function encryptAttachments(file, callback) {
3335
            if (typeof file !== 'undefined' && file !== null) {
3336
                // check file reader requirements for upload
3337
                if (typeof FileReader === 'undefined') {
3338
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3339
                    // cancels process as it does not execute callback
3340
                    return;
3341
                }
3342
3343
                var reader = new FileReader();
3344
3345
                // closure to capture the file information
3346
                reader.onload = function(event) {
3347
                    Uploader.setData('attachment', event.target.result);
3348
                    Uploader.setData('attachmentname', file.name);
3349
3350
                    // run callback
3351
                    return callback();
3352
                };
3353
3354
                // actually read first file
3355
                reader.readAsDataURL(file);
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
3356
            } else if (AttachmentViewer.hasAttachment()) {
3357
                // fall back to cloned part
3358
                var attachment = AttachmentViewer.getAttachment();
3359
3360
                Uploader.setData('attachment', attachment[0]);
3361
                Uploader.setData('attachmentname', attachment[1]);
3362
                return callback();
3363
            } else {
3364
                // if there are no attachments, this is of course still successful
3365
                return callback();
3366
            }
3367
        }
3368
3369
        /**
3370
         * send a reply in a discussion
3371
         *
3372
         * @name   PasteEncrypter.sendComment
3373
         * @function
3374
         */
3375
        me.sendComment = function()
3376
        {
3377
            Alert.hideMessages();
3378
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3379
3380
            // UI loading state
3381
            TopNav.hideAllButtons();
3382
            Alert.showLoading('Sending comment…', 'cloud-upload');
3383
3384
            // get data
3385
            var plainText = DiscussionViewer.getReplyMessage(),
3386
                nickname = DiscussionViewer.getReplyNickname(),
3387
                parentid = DiscussionViewer.getReplyCommentId();
3388
3389
            // do not send if there is no data
3390
            if (plainText.length === 0) {
3391
                // revert loading status…
3392
                Alert.hideLoading();
3393
                Alert.setCustomHandler(null);
3394
                TopNav.showViewButtons();
3395
                return;
3396
            }
3397
3398
            // check entropy
3399
            if (!checkRequirements(function () {
3400
                me.sendComment();
3401
            })) {
3402
                return; // to prevent multiple executions
3403
            }
3404
3405
            // prepare Uploader
3406
            Uploader.prepare();
3407
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3408
3409
            // set success/fail functions
3410
            Uploader.setSuccess(showUploadedComment);
3411
            Uploader.setFailure(function (status, data) {
3412
                // revert loading status…
3413
                Alert.hideLoading();
3414
                TopNav.showViewButtons();
3415
3416
                // show error message
3417
                Alert.showError(
3418
                    Uploader.parseUploadError(status, data, 'post comment')
3419
                );
3420
3421
                // reset error handler
3422
                Alert.setCustomHandler(null);
3423
            });
3424
3425
            // fill it with unencrypted params
3426
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3427
            if (typeof parentid === 'undefined') {
3428
                // if parent id is not set, this is the top-most comment, so use
3429
                // paste id as parent, as the root element of the discussion tree
3430
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3431
            } else {
3432
                Uploader.setUnencryptedData('parentid', parentid);
3433
            }
3434
3435
            // encrypt data
3436
            Uploader.setData('data', plainText);
3437
3438
            if (nickname.length > 0) {
3439
                Uploader.setData('nickname', nickname);
3440
            }
3441
3442
            Uploader.run();
3443
        };
3444
3445
        /**
3446
         * sends a new paste to server
3447
         *
3448
         * @name   PasteEncrypter.sendPaste
3449
         * @function
3450
         */
3451
        me.sendPaste = function()
3452
        {
3453
            // hide previous (error) messages
3454
            Controller.hideStatusMessages();
3455
3456
            // UI loading state
3457
            TopNav.hideAllButtons();
3458
            Alert.showLoading('Sending paste…', 'cloud-upload');
3459
            TopNav.collapseBar();
3460
3461
            // get data
3462
            var plainText = Editor.getText(),
3463
                format = PasteViewer.getFormat(),
3464
                files = TopNav.getFileList();
3465
3466
            // do not send if there is no data
3467
            if (plainText.length === 0 && files === null) {
3468
                // revert loading status…
3469
                Alert.hideLoading();
3470
                TopNav.showCreateButtons();
3471
                return;
3472
            }
3473
3474
            // check entropy
3475
            if (!checkRequirements(function () {
3476
                me.sendPaste();
3477
            })) {
3478
                return; // to prevent multiple executions
3479
            }
3480
3481
            // prepare Uploader
3482
            Uploader.prepare();
3483
            Uploader.setCryptParameters(TopNav.getPassword());
3484
3485
            // set success/fail functions
3486
            Uploader.setSuccess(showCreatedPaste);
3487
            Uploader.setFailure(function (status, data) {
3488
                // revert loading status…
3489
                Alert.hideLoading();
3490
                TopNav.showCreateButtons();
3491
3492
                // show error message
3493
                Alert.showError(
3494
                    Uploader.parseUploadError(status, data, 'create paste')
3495
                );
3496
            });
3497
3498
            // fill it with unencrypted submitted options
3499
            Uploader.setUnencryptedBulkData({
3500
                expire:           TopNav.getExpiration(),
3501
                formatter:        format,
3502
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3503
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3504
            });
3505
3506
            // prepare PasteViewer for later preview
3507
            PasteViewer.setText(plainText);
3508
            PasteViewer.setFormat(format);
3509
3510
            // encrypt cipher data
3511
            Uploader.setData('data', plainText);
3512
3513
            // encrypt attachments
3514
            encryptAttachments(
3515
                files === null ? null : files[0],
3516
                function () {
3517
                    // send data
3518
                    Uploader.run();
3519
                }
3520
            );
3521
        };
3522
3523
        /**
3524
         * initialize
3525
         *
3526
         * @name   PasteEncrypter.init
3527
         * @function
3528
         */
3529
        me.init = function()
3530
        {
3531
            // nothing yet
3532
        };
3533
3534
        return me;
3535
    })();
3536
3537
    /**
3538
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3539
     *
3540
     * Only decryption, no download.
3541
     *
3542
     * @name PasteDecrypter
3543
     * @class
3544
     */
3545
    var PasteDecrypter = (function () {
3546
        var me = {};
3547
3548
        /**
3549
         * decrypt data or prompts for password in cvase of failure
3550
         *
3551
         * @name   PasteDecrypter.decryptOrPromptPassword
3552
         * @private
3553
         * @function
3554
         * @param  {string} key
3555
         * @param  {string} password - optional, may be an empty string
3556
         * @param  {string} cipherdata
3557
         * @throws {string}
3558
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3559
         */
3560
        function decryptOrPromptPassword(key, password, cipherdata)
3561
        {
3562
            // try decryption without password
3563
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3564
3565
            // if it fails, request password
3566
            if (plaindata.length === 0 && password.length === 0) {
3567
                // try to get cached password first
3568
                password = Prompt.getPassword();
3569
3570
                // if password is there, re-try
3571
                if (password.length === 0) {
3572
                    password = Prompt.requestPassword();
3573
                }
3574
                // recursive
3575
                // note: an infinite loop is prevented as the previous if
3576
                // clause checks whether a password is already set and ignores
3577
                // errors when a password has been passed
3578
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3579
            }
3580
3581
            // if all tries failed, we can only return an error
3582
            if (plaindata.length === 0) {
3583
                throw 'failed to decipher data';
3584
            }
3585
3586
            return plaindata;
3587
        }
3588
3589
        /**
3590
         * decrypt the actual paste text
3591
         *
3592
         * @name   PasteDecrypter.decryptOrPromptPassword
3593
         * @private
3594
         * @function
3595
         * @param  {object} paste - paste data in object form
3596
         * @param  {string} key
3597
         * @param  {string} password
3598
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3599
         * @return {bool} whether action was successful
3600
         * @throws {string}
3601
         */
3602
        function decryptPaste(paste, key, password, ignoreError)
3603
        {
3604
            var plaintext;
3605
            if (ignoreError === true) {
3606
                plaintext = CryptTool.decipher(key, password, paste.data);
3607
            } else {
3608
                try {
3609
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3610
                } catch (err) {
3611
                    throw 'failed to decipher paste text: ' + err;
3612
                }
3613
                if (plaintext === false) {
3614
                    return false;
3615
                }
3616
            }
3617
3618
            // on success show paste
3619
            PasteViewer.setFormat(paste.meta.formatter);
3620
            PasteViewer.setText(plaintext);
3621
            // trigger to show the text (attachment loaded afterwards)
3622
            PasteViewer.run();
3623
3624
            return true;
3625
        }
3626
3627
        /**
3628
         * decrypts any attachment
3629
         *
3630
         * @name   PasteDecrypter.decryptAttachment
3631
         * @private
3632
         * @function
3633
         * @param  {object} paste - paste data in object form
3634
         * @param  {string} key
3635
         * @param  {string} password
3636
         * @return {bool} whether action was successful
3637
         * @throws {string}
3638
         */
3639
        function decryptAttachment(paste, key, password)
3640
        {
3641
            var attachment, attachmentName;
3642
3643
            // decrypt attachment
3644
            try {
3645
                attachment = decryptOrPromptPassword(key, password, paste.attachment);
3646
            } catch (err) {
3647
                throw 'failed to decipher attachment: ' + err;
3648
            }
3649
            if (attachment === false) {
3650
                return false;
3651
            }
3652
3653
            // decrypt attachment name
3654
            if (paste.attachmentname) {
3655
                try {
3656
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3657
                } catch (err) {
3658
                    throw 'failed to decipher attachment name: ' + err;
3659
                }
3660
                if (attachmentName === false) {
3661
                    return false;
3662
                }
3663
            }
3664
3665
            AttachmentViewer.setAttachment(attachment, attachmentName);
3666
            AttachmentViewer.showAttachment();
3667
3668
            return true;
3669
        }
3670
3671
        /**
3672
         * decrypts all comments and shows them
3673
         *
3674
         * @name   PasteDecrypter.decryptComments
3675
         * @private
3676
         * @function
3677
         * @param  {object} paste - paste data in object form
3678
         * @param  {string} key
3679
         * @param  {string} password
3680
         * @return {bool} whether action was successful
3681
         */
3682
        function decryptComments(paste, key, password)
3683
        {
3684
            // remove potentially previous discussion
3685
            DiscussionViewer.prepareNewDiscussion();
3686
3687
            // iterate over comments
3688
            for (var i = 0; i < paste.comments.length; ++i) {
3689
                var comment = paste.comments[i];
3690
3691
                DiscussionViewer.addComment(
3692
                    comment,
3693
                    CryptTool.decipher(key, password, comment.data),
3694
                    CryptTool.decipher(key, password, comment.meta.nickname)
3695
                );
3696
            }
3697
3698
            DiscussionViewer.finishDiscussion();
3699
            return true;
3700
        }
3701
3702
        /**
3703
         * show decrypted text in the display area, including discussion (if open)
3704
         *
3705
         * @name   PasteDecrypter.run
3706
         * @function
3707
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3708
         */
3709
        me.run = function(paste)
3710
        {
3711
            Alert.hideMessages();
3712
            Alert.showLoading('Decrypting paste…', 'cloud-download');
3713
3714
            if (typeof paste === 'undefined') {
3715
                paste = $.parseJSON(Model.getCipherData());
3716
            }
3717
3718
            var key = Model.getPasteKey(),
3719
                password = Prompt.getPassword();
3720
3721
            if (PasteViewer.isPrettyPrinted()) {
3722
                // don't decrypt twice
3723
                return;
3724
            }
3725
3726
            // try to decrypt the paste
3727
            try {
3728
                // decrypt attachments
3729
                if (paste.attachment) {
3730
                    // try to decrypt paste and if it fails (because the password is
3731
                    // missing) return to let JS continue and wait for user
3732
                    if (!decryptAttachment(paste, key, password)) {
3733
                        return;
3734
                    }
3735
                    // ignore empty paste, as this is allowed when pasting attachments
3736
                    decryptPaste(paste, key, password, true);
3737
                } else {
3738
                    decryptPaste(paste, key, password);
3739
                }
3740
3741
3742
                // shows the remaining time (until) deletion
3743
                PasteStatus.showRemainingTime(paste.meta);
3744
3745
                // if the discussion is opened on this paste, display it
3746
                if (paste.meta.opendiscussion) {
3747
                    decryptComments(paste, key, password);
3748
                }
3749
3750
                Alert.hideLoading();
3751
                TopNav.showViewButtons();
3752
            } catch(err) {
3753
                Alert.hideLoading();
3754
3755
                // log and show error
3756
                console.error(err);
3757
                Alert.showError('Could not decrypt data (Wrong key?)');
3758
            }
3759
        };
3760
3761
        /**
3762
         * initialize
3763
         *
3764
         * @name   PasteDecrypter.init
3765
         * @function
3766
         */
3767
        me.init = function()
3768
        {
3769
            // nothing yet
3770
        };
3771
3772
        return me;
3773
    })();
3774
3775
    /**
3776
     * (controller) main PrivateBin logic
3777
     *
3778
     * @name   Controller
3779
     * @param  {object} window
3780
     * @param  {object} document
3781
     * @class
3782
     */
3783
    var Controller = (function (window, document) {
3784
        var me = {};
3785
3786
        /**
3787
         * hides all status messages no matter which module showed them
3788
         *
3789
         * @name   Controller.hideStatusMessages
3790
         * @function
3791
         */
3792
        me.hideStatusMessages = function()
3793
        {
3794
            PasteStatus.hideMessages();
3795
            Alert.hideMessages();
3796
        };
3797
3798
        /**
3799
         * creates a new paste
3800
         *
3801
         * @name   Controller.newPaste
3802
         * @function
3803
         */
3804
        me.newPaste = function()
3805
        {
3806
            // Important: This *must not* run Alert.hideMessages() as previous
3807
            // errors from viewing a paste should be shown.
3808
            TopNav.hideAllButtons();
3809
            Alert.showLoading('Preparing new paste…', 'time');
3810
3811
            PasteStatus.hideMessages();
3812
            PasteViewer.hide();
3813
            Editor.resetInput();
3814
            Editor.show();
3815
            Editor.focusInput();
3816
3817
            TopNav.showCreateButtons();
3818
            Alert.hideLoading();
3819
        };
3820
3821
        /**
3822
         * shows the loaded paste
3823
         *
3824
         * @name   Controller.showPaste
3825
         * @function
3826
         */
3827
        me.showPaste = function()
3828
        {
3829
            try {
3830
                Model.getPasteId();
3831
                Model.getPasteKey();
3832
            } catch (err) {
3833
                console.error(err);
3834
3835
                // missing decryption key (or paste ID) in URL?
3836
                if (window.location.hash.length === 0) {
3837
                    Alert.showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)');
3838
                    return;
3839
                }
3840
            }
3841
3842
            // show proper elements on screen
3843
            PasteDecrypter.run();
3844
        };
3845
3846
        /**
3847
         * refreshes the loaded paste to show potential new data
3848
         *
3849
         * @name   Controller.refreshPaste
3850
         * @function
3851
         * @param  {function} callback
3852
         */
3853
        me.refreshPaste = function(callback)
3854
        {
3855
            // save window position to restore it later
3856
            var orgPosition = $(window).scrollTop();
3857
3858
            Uploader.prepare();
3859
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3860
3861
            Uploader.setFailure(function (status, data) {
3862
                // revert loading status…
3863
                Alert.hideLoading();
3864
                TopNav.showViewButtons();
3865
3866
                // show error message
3867
                Alert.showError(
3868
                    Uploader.parseUploadError(status, data, 'refresh display')
3869
                );
3870
            });
3871
            Uploader.setSuccess(function (status, data) {
3872
                PasteDecrypter.run(data);
3873
3874
                // restore position
3875
                window.scrollTo(0, orgPosition);
3876
3877
                callback();
3878
            });
3879
            Uploader.run();
3880
        };
3881
3882
        /**
3883
         * clone the current paste
3884
         *
3885
         * @name   Controller.clonePaste
3886
         * @function
3887
         */
3888
        me.clonePaste = function()
3889
        {
3890
            TopNav.collapseBar();
3891
            TopNav.hideAllButtons();
3892
            Alert.showLoading('Cloning paste…', 'transfer');
3893
3894
            // hide messages from previous paste
3895
            me.hideStatusMessages();
3896
3897
            // erase the id and the key in url
3898
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3899
3900
            if (AttachmentViewer.hasAttachment()) {
3901
                AttachmentViewer.moveAttachmentTo(
3902
                    TopNav.getCustomAttachment(),
3903
                    'Cloned: \'%s\''
3904
                );
3905
                TopNav.hideFileSelector();
3906
                AttachmentViewer.hideAttachment();
3907
                // NOTE: it also looks nice without removing the attachment
3908
                // but for a consistent display we remove it…
3909
                AttachmentViewer.hideAttachmentPreview();
3910
                TopNav.showCustomAttachment();
3911
3912
                // show another status message to make the user aware that the
3913
                // file was cloned too!
3914
                Alert.showStatus(
3915
                    [
3916
                        'The cloned file \'%s\' was attached to this paste.',
3917
                        AttachmentViewer.getAttachment()[1]
3918
                    ],
3919
                    'copy'
3920
                );
3921
            }
3922
3923
            Editor.setText(PasteViewer.getText());
3924
            PasteViewer.hide();
3925
            Editor.show();
3926
3927
            Alert.hideLoading();
3928
            TopNav.showCreateButtons();
3929
        };
3930
3931
        /**
3932
         * removes a saved paste
3933
         *
3934
         * @name   Controller.removePaste
3935
         * @function
3936
         * @param  {string} pasteId
3937
         * @param  {string} deleteToken
3938
         */
3939
        me.removePaste = function(pasteId, deleteToken) {
3940
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3941
            // so we use a POST request
3942
            Uploader.prepare();
3943
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3944
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3945
3946
            Uploader.setFailure(function () {
3947
                Alert.showError(
3948
                    I18n._('Could not delete the paste, it was not stored in burn after reading mode.')
3949
                );
3950
            });
3951
            Uploader.run();
3952
        };
3953
3954
        /**
3955
         * application start
3956
         *
3957
         * @name   Controller.init
3958
         * @function
3959
         */
3960
        me.init = function()
3961
        {
3962
            // first load translations
3963
            I18n.loadTranslations();
3964
3965
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
3966
3967
            // initialize other modules/"classes"
3968
            Alert.init();
3969
            Model.init();
3970
            AttachmentViewer.init();
3971
            DiscussionViewer.init();
3972
            Editor.init();
3973
            PasteDecrypter.init();
3974
            PasteEncrypter.init();
3975
            PasteStatus.init();
3976
            PasteViewer.init();
3977
            Prompt.init();
3978
            TopNav.init();
3979
            UiHelper.init();
3980
            Uploader.init();
3981
3982
            // display an existing paste
3983
            if (Model.hasCipherData()) {
3984
                return me.showPaste();
3985
            }
3986
3987
            // otherwise create a new paste
3988
            me.newPaste();
3989
        };
3990
3991
        return me;
3992
    })(window, document);
3993
3994
    return {
3995
        Helper: Helper,
3996
        I18n: I18n,
3997
        CryptTool: CryptTool,
3998
        Model: Model,
3999
        UiHelper: UiHelper,
4000
        Alert: Alert,
4001
        PasteStatus: PasteStatus,
4002
        Prompt: Prompt,
4003
        Editor: Editor,
4004
        PasteViewer: PasteViewer,
4005
        AttachmentViewer: AttachmentViewer,
4006
        DiscussionViewer: DiscussionViewer,
4007
        TopNav: TopNav,
4008
        Uploader: Uploader,
4009
        PasteEncrypter: PasteEncrypter,
4010
        PasteDecrypter: PasteDecrypter,
4011
        Controller: Controller
4012
    };
4013
})(jQuery, sjcl, Base64, RawDeflate);
4014