Passed
Push — master ( 57754f...4ccb4b )
by El
03:38
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', 'nl', '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
            if (AttachmentViewer.hasAttachmentData()) {
1524
                var attachmentData = AttachmentViewer.getAttachmentData() || AttachmentViewer.getAttachmentLink().attr('href');
1525
                AttachmentViewer.handleAttachmentPreview(AttachmentViewer.getAttachmentPreview(), attachmentData);
1526
            }
1527
            PasteViewer.run();
1528
1529
            // finish
1530
            isPreview = true;
1531
1532
            // prevent jumping of page to top
1533
            if (typeof event !== 'undefined') {
1534
                event.preventDefault();
1535
            }
1536
        }
1537
1538
        /**
1539
         * get the state of the preview
1540
         *
1541
         * @name   Editor.isPreview
1542
         * @function
1543
         */
1544
        me.isPreview = function()
1545
        {
1546
            return isPreview;
1547
        };
1548
1549
        /**
1550
         * reset the Editor view
1551
         *
1552
         * @name   Editor.resetInput
1553
         * @function
1554
         */
1555
        me.resetInput = function()
1556
        {
1557
            // go back to input
1558
            if (isPreview) {
1559
                viewEditor();
1560
            }
1561
1562
            // clear content
1563
            $message.val('');
1564
        };
1565
1566
        /**
1567
         * shows the Editor
1568
         *
1569
         * @name   Editor.show
1570
         * @function
1571
         */
1572
        me.show = function()
1573
        {
1574
            $message.removeClass('hidden');
1575
            $editorTabs.removeClass('hidden');
1576
        };
1577
1578
        /**
1579
         * hides the Editor
1580
         *
1581
         * @name   Editor.reset
1582
         * @function
1583
         */
1584
        me.hide = function()
1585
        {
1586
            $message.addClass('hidden');
1587
            $editorTabs.addClass('hidden');
1588
        };
1589
1590
        /**
1591
         * focuses the message input
1592
         *
1593
         * @name   Editor.focusInput
1594
         * @function
1595
         */
1596
        me.focusInput = function()
1597
        {
1598
            $message.focus();
1599
        };
1600
1601
        /**
1602
         * sets a new text
1603
         *
1604
         * @name   Editor.setText
1605
         * @function
1606
         * @param {string} newText
1607
         */
1608
        me.setText = function(newText)
1609
        {
1610
            $message.val(newText);
1611
        };
1612
1613
        /**
1614
         * returns the current text
1615
         *
1616
         * @name   Editor.getText
1617
         * @function
1618
         * @return {string}
1619
         */
1620
        me.getText = function()
1621
        {
1622
            return $message.val();
1623
        };
1624
1625
        /**
1626
         * init status manager
1627
         *
1628
         * preloads jQuery elements
1629
         *
1630
         * @name   Editor.init
1631
         * @function
1632
         */
1633
        me.init = function()
1634
        {
1635
            $editorTabs = $('#editorTabs');
1636
            $message = $('#message');
1637
1638
            // bind events
1639
            $message.keydown(supportTabs);
1640
1641
            // bind click events to tab switchers (a), but save parent of them
1642
            // (li)
1643
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1644
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1645
        };
1646
1647
        return me;
1648
    })();
1649
1650
    /**
1651
     * (view) Parse and show paste.
1652
     *
1653
     * @name   PasteViewer
1654
     * @class
1655
     */
1656
    var PasteViewer = (function () {
1657
        var me = {};
1658
1659
        var $placeholder,
1660
            $prettyMessage,
1661
            $prettyPrint,
1662
            $plainText;
1663
1664
        var text,
1665
            format = 'plaintext',
1666
            isDisplayed = false,
1667
            isChanged = true; // by default true as nothing was parsed yet
1668
1669
        /**
1670
         * apply the set format on paste and displays it
1671
         *
1672
         * @name   PasteViewer.parsePaste
1673
         * @private
1674
         * @function
1675
         */
1676
        function parsePaste()
1677
        {
1678
            // skip parsing if no text is given
1679
            if (text === '') {
1680
                return;
1681
            }
1682
1683
            // escape HTML entities, link URLs, sanitize
1684
            var escapedLinkedText = Helper.urls2links(
1685
                    $('<div />').text(text).html()
1686
                ),
1687
                sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText);
1688
            $plainText.html(sanitizedLinkedText);
1689
            $prettyPrint.html(sanitizedLinkedText);
1690
1691
            switch (format) {
1692
                case 'markdown':
1693
                    var converter = new showdown.Converter({
1694
                        strikethrough: true,
1695
                        tables: true,
1696
                        tablesHeaderId: true
1697
                    });
1698
                    // let showdown convert the HTML and sanitize HTML *afterwards*!
1699
                    $plainText.html(
1700
                        DOMPurify.sanitize(converter.makeHtml(text))
1701
                    );
1702
                    // add table classes from bootstrap css
1703
                    $plainText.find('table').addClass('table-condensed table-bordered');
1704
                    break;
1705
                case 'syntaxhighlighting':
1706
                    // yes, this is really needed to initialize the environment
1707
                    if (typeof prettyPrint === 'function')
1708
                    {
1709
                        prettyPrint();
1710
                    }
1711
1712
                    $prettyPrint.html(
1713
                        DOMPurify.sanitize(
1714
                            prettyPrintOne(escapedLinkedText, null, true)
1715
                        )
1716
                    );
1717
                    // fall through, as the rest is the same
1718
                default: // = 'plaintext'
1719
                    $prettyPrint.css('white-space', 'pre-wrap');
1720
                    $prettyPrint.css('word-break', 'normal');
1721
                    $prettyPrint.removeClass('prettyprint');
1722
            }
1723
        }
1724
1725
        /**
1726
         * displays the paste
1727
         *
1728
         * @name   PasteViewer.showPaste
1729
         * @private
1730
         * @function
1731
         */
1732
        function showPaste()
1733
        {
1734
            // instead of "nothing" better display a placeholder
1735
            if (text === '') {
1736
                $placeholder.removeClass('hidden');
1737
                return;
1738
            }
1739
            // otherwise hide the placeholder
1740
            $placeholder.addClass('hidden');
1741
1742
            switch (format) {
1743
                case 'markdown':
1744
                    $plainText.removeClass('hidden');
1745
                    $prettyMessage.addClass('hidden');
1746
                    break;
1747
                default:
1748
                    $plainText.addClass('hidden');
1749
                    $prettyMessage.removeClass('hidden');
1750
                    break;
1751
            }
1752
        }
1753
1754
        /**
1755
         * sets the format in which the text is shown
1756
         *
1757
         * @name   PasteViewer.setFormat
1758
         * @function
1759
         * @param {string} newFormat the new format
1760
         */
1761
        me.setFormat = function(newFormat)
1762
        {
1763
            // skip if there is no update
1764
            if (format === newFormat) {
1765
                return;
1766
            }
1767
1768
            // needs to update display too, if we switch from or to Markdown
1769
            if (format === 'markdown' || newFormat === 'markdown') {
1770
                isDisplayed = false;
1771
            }
1772
1773
            format = newFormat;
1774
            isChanged = true;
1775
        };
1776
1777
        /**
1778
         * returns the current format
1779
         *
1780
         * @name   PasteViewer.getFormat
1781
         * @function
1782
         * @return {string}
1783
         */
1784
        me.getFormat = function()
1785
        {
1786
            return format;
1787
        };
1788
1789
        /**
1790
         * returns whether the current view is pretty printed
1791
         *
1792
         * @name   PasteViewer.isPrettyPrinted
1793
         * @function
1794
         * @return {bool}
1795
         */
1796
        me.isPrettyPrinted = function()
1797
        {
1798
            return $prettyPrint.hasClass('prettyprinted');
1799
        };
1800
1801
        /**
1802
         * sets the text to show
1803
         *
1804
         * @name   PasteViewer.setText
1805
         * @function
1806
         * @param {string} newText the text to show
1807
         */
1808
        me.setText = function(newText)
1809
        {
1810
            if (text !== newText) {
1811
                text = newText;
1812
                isChanged = true;
1813
            }
1814
        };
1815
1816
        /**
1817
         * gets the current cached text
1818
         *
1819
         * @name   PasteViewer.getText
1820
         * @function
1821
         * @return {string}
1822
         */
1823
        me.getText = function()
1824
        {
1825
            return text;
1826
        };
1827
1828
        /**
1829
         * show/update the parsed text (preview)
1830
         *
1831
         * @name   PasteViewer.run
1832
         * @function
1833
         */
1834
        me.run = function()
1835
        {
1836
            if (isChanged) {
1837
                parsePaste();
1838
                isChanged = false;
1839
            }
1840
1841
            if (!isDisplayed) {
1842
                showPaste();
1843
                isDisplayed = true;
1844
            }
1845
        };
1846
1847
        /**
1848
         * hide parsed text (preview)
1849
         *
1850
         * @name   PasteViewer.hide
1851
         * @function
1852
         */
1853
        me.hide = function()
1854
        {
1855
            if (!isDisplayed) {
1856
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1857
            }
1858
1859
            $plainText.addClass('hidden');
1860
            $prettyMessage.addClass('hidden');
1861
            $placeholder.addClass('hidden');
1862
            AttachmentViewer.hideAttachmentPreview();
1863
1864
            isDisplayed = false;
1865
        };
1866
1867
        /**
1868
         * init status manager
1869
         *
1870
         * preloads jQuery elements
1871
         *
1872
         * @name   PasteViewer.init
1873
         * @function
1874
         */
1875
        me.init = function()
1876
        {
1877
            $placeholder = $('#placeholder');
1878
            $plainText = $('#plaintext');
1879
            $prettyMessage = $('#prettymessage');
1880
            $prettyPrint = $('#prettyprint');
1881
1882
            // check requirements
1883
            if (typeof prettyPrintOne !== 'function') {
1884
                Alert.showError([
1885
                    'The library %s is not available. This may cause display errors.',
1886
                    'pretty print'
1887
                ]);
1888
            }
1889
            if (typeof showdown !== 'object') {
1890
                Alert.showError([
1891
                    'The library %s is not available. This may cause display errors.',
1892
                    'showdown'
1893
                ]);
1894
            }
1895
1896
            // get default option from template/HTML or fall back to set value
1897
            format = Model.getFormatDefault() || format;
1898
            text = '';
1899
            isDisplayed = false;
1900
            isChanged = true;
1901
        };
1902
1903
        return me;
1904
    })();
1905
1906
    /**
1907
     * (view) Show attachment and preview if possible
1908
     *
1909
     * @name   AttachmentViewer
1910
     * @class
1911
     */
1912
    var AttachmentViewer = (function () {
1913
        var me = {};
1914
1915
        var $attachmentLink;
1916
        var $attachmentPreview;
1917
        var $attachment;
1918
        var attachmentData;
1919
        var file;
1920
        var $fileInput;
1921
        var $dragAndDropFileName;
1922
        var attachmentHasPreview = false;
1923
1924
        /**
1925
         * sets the attachment but does not yet show it
1926
         *
1927
         * @name   AttachmentViewer.setAttachment
1928
         * @function
1929
         * @param {string} attachmentData - base64-encoded data of file
1930
         * @param {string} fileName - optional, file name
1931
         */
1932
        me.setAttachment = function(attachmentData, fileName)
1933
        {
1934
            // IE does not support setting a data URI on an a element
1935
            // Convert dataURI to a Blob and use msSaveBlob to download
1936
            if (window.Blob && navigator.msSaveBlob) {
1937
                $attachmentLink.off('click').on('click', function () {
1938
                    // data URI format: data:[<mediaType>][;base64],<data>
1939
1940
                    // position in data URI string of where data begins
1941
                    var base64Start = attachmentData.indexOf(',') + 1;
1942
                    // position in data URI string of where mediaType ends
1943
                    var mediaTypeEnd = attachmentData.indexOf(';');
1944
1945
                    // extract mediaType
1946
                    var mediaType = attachmentData.substring(5, mediaTypeEnd);
1947
                    // extract data and convert to binary
1948
                    var decodedData = Base64.atob(attachmentData.substring(base64Start));
1949
1950
                    // Transform into a Blob
1951
                    var decodedDataLength = decodedData.length;
1952
                    var buf = new Uint8Array(decodedDataLength);
1953
1954
                    for (var i = 0; i < decodedDataLength; i++) {
1955
                        buf[i] = decodedData.charCodeAt(i);
1956
                    }
1957
1958
                    var blob = new window.Blob([ buf ], { type: mediaType });
1959
                    navigator.msSaveBlob(blob, fileName);
1960
                });
1961
            } else {
1962
                $attachmentLink.attr('href', attachmentData);
1963
            }
1964
1965
            if (typeof fileName !== 'undefined') {
1966
                $attachmentLink.attr('download', fileName);
1967
            }
1968
1969
            me.handleAttachmentPreview($attachmentPreview, attachmentData);
1970
        };
1971
1972
        /**
1973
         * displays the attachment
1974
         *
1975
         * @name AttachmentViewer.showAttachment
1976
         * @function
1977
         */
1978
        me.showAttachment = function()
1979
        {
1980
            $attachment.removeClass('hidden');
1981
1982
            if (attachmentHasPreview) {
1983
                $attachmentPreview.removeClass('hidden');
1984
            }
1985
        };
1986
1987
        /**
1988
         * removes the attachment
1989
         *
1990
         * This automatically hides the attachment containers too, to
1991
         * prevent an inconsistent display.
1992
         *
1993
         * @name AttachmentViewer.removeAttachment
1994
         * @function
1995
         */
1996
        me.removeAttachment = function()
1997
        {
1998
            if (!$attachment.length) {
1999
                return;
2000
            }
2001
            me.hideAttachment();
2002
            me.hideAttachmentPreview();
2003
            $attachmentLink.removeAttr('href');
2004
            $attachmentLink.removeAttr('download');
2005
            $attachmentLink.off('click');
2006
            $attachmentPreview.html('');
2007
2008
            AttachmentViewer.removeAttachmentData();
2009
        };
2010
2011
        /**
2012
         * removes the attachment data
2013
         *
2014
         * This removes the data, which would be uploaded otherwise.
2015
         *
2016
         * @name AttachmentViewer.removeAttachmentData
2017
         * @function
2018
         */
2019
        me.removeAttachmentData = function()
2020
        {
2021
            file = undefined;
2022
            attachmentData = undefined;
2023
        };
2024
2025
        /**
2026
         * Cleares the drag & drop data.
2027
         *
2028
         * @name AttachmentViewer.clearDragAndDrop
2029
         * @function
2030
         */
2031
        me.clearDragAndDrop = function()
2032
        {
2033
            $dragAndDropFileName.text('');
2034
        };
2035
2036
        /**
2037
         * hides the attachment
2038
         *
2039
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2040
         * for that) nor will it hide the attachment link if it was moved somewhere
2041
         * else (see AttachmentViewer.moveAttachmentTo).
2042
         *
2043
         * @name AttachmentViewer.hideAttachment
2044
         * @function
2045
         */
2046
        me.hideAttachment = function()
2047
        {
2048
            $attachment.addClass('hidden');
2049
        };
2050
2051
        /**
2052
         * hides the attachment preview
2053
         *
2054
         * @name AttachmentViewer.hideAttachmentPreview
2055
         * @function
2056
         */
2057
        me.hideAttachmentPreview = function()
2058
        {
2059
            if ($attachmentPreview) {
2060
                $attachmentPreview.addClass('hidden');
2061
            }
2062
        };
2063
2064
        /**
2065
         * checks if there is an attachment displayed
2066
         *
2067
         * @name   AttachmentViewer.hasAttachment
2068
         * @function
2069
         */
2070
        me.hasAttachment = function()
2071
        {
2072
            if (!$attachment.length) {
2073
                return false;
2074
            }
2075
            var link = $attachmentLink.prop('href');
2076
            return (typeof link !== 'undefined' && link !== '');
2077
        };
2078
2079
        /**
2080
         * checks if there is attachment data (for preview!) available
2081
         *
2082
         * It returns true, when there is data that needs to be encrypted.
2083
         *
2084
         * @name   AttachmentViewer.hasAttachmentData
2085
         * @function
2086
         */
2087
        me.hasAttachmentData = function()
2088
        {
2089
            if ($attachment.length) {
2090
                return true;
2091
            }
2092
            return false;
2093
        };
2094
2095
        /**
2096
         * return the attachment
2097
         *
2098
         * @name   AttachmentViewer.getAttachment
2099
         * @function
2100
         * @returns {array}
2101
         */
2102
        me.getAttachment = function()
2103
        {
2104
            return [
2105
                $attachmentLink.prop('href'),
2106
                $attachmentLink.prop('download')
2107
            ];
2108
        };
2109
2110
        /**
2111
         * moves the attachment link to another element
2112
         *
2113
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2114
         *
2115
         * @name   AttachmentViewer.moveAttachmentTo
2116
         * @function
2117
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2118
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2119
         */
2120
        me.moveAttachmentTo = function($element, label)
2121
        {
2122
            // move elemement to new place
2123
            $attachmentLink.appendTo($element);
2124
2125
            // update text
2126
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2127
        };
2128
2129
        /**
2130
         * read file data as dataURL using the FileReader API
2131
         *
2132
         * @name   AttachmentViewer.readFileData
2133
         * @private
2134
         * @function
2135
         * @param {object} loadedFile The loaded file.
2136
         * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL()}
2137
         */
2138
        function readFileData(loadedFile) {
2139
            if (typeof FileReader === 'undefined') {
2140
                // revert loading status…
2141
                me.hideAttachment();
2142
                me.hideAttachmentPreview();
2143
                Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
2144
                return;
2145
            }
2146
2147
            var fileReader = new FileReader();
2148
            if (loadedFile === undefined) {
2149
                loadedFile = $fileInput[0].files[0];
2150
                $dragAndDropFileName.text('');
2151
            } else {
2152
                // TODO: cannot set original $fileWrap here for security reasons…
2153
                $dragAndDropFileName.text(loadedFile.name);
2154
            }
2155
2156
            file = loadedFile;
2157
2158
            fileReader.onload = function (event) {
2159
                var dataURL = event.target.result;
2160
                attachmentData = dataURL;
2161
2162
                if (Editor.isPreview()) {
2163
                    me.handleAttachmentPreview($attachmentPreview, dataURL);
2164
                    $attachmentPreview.removeClass('hidden');
2165
                }
2166
            };
2167
            fileReader.readAsDataURL(loadedFile);
2168
        }
2169
2170
        /**
2171
         * handle the preview of files that can either be an image, video, audio or pdf element
2172
         *
2173
         * @name   AttachmentViewer.handleAttachmentPreview
2174
         * @function
2175
         * @argument {jQuery} $targetElement where the preview should be appended.
2176
         * @argument {File Data} data of the file to be displayed.
2177
         */
2178
        me.handleAttachmentPreview = function ($targetElement, data) {
2179
            if (data) {
2180
                // source: https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL()
2181
                var mimeType = data.slice(
2182
                    data.indexOf('data:') + 5,
2183
                    data.indexOf(';base64,')
2184
                );
2185
2186
                attachmentHasPreview = true;
2187
                if (mimeType.match(/image\//i)) {
2188
                    $targetElement.html(
2189
                        $(document.createElement('img'))
2190
                            .attr('src', data)
2191
                            .attr('class', 'img-thumbnail')
2192
                    );
2193
                } else if (mimeType.match(/video\//i)) {
2194
                    $targetElement.html(
2195
                        $(document.createElement('video'))
2196
                            .attr('controls', 'true')
2197
                            .attr('autoplay', 'true')
2198
                            .attr('class', 'img-thumbnail')
2199
2200
                            .append($(document.createElement('source'))
2201
                            .attr('type', mimeType)
2202
                            .attr('src', data))
2203
                    );
2204
                } else if (mimeType.match(/audio\//i)) {
2205
                    $targetElement.html(
2206
                        $(document.createElement('audio'))
2207
                            .attr('controls', 'true')
2208
                            .attr('autoplay', 'true')
2209
2210
                            .append($(document.createElement('source'))
2211
                            .attr('type', mimeType)
2212
                            .attr('src', data))
2213
                    );
2214
                } else if (mimeType.match(/\/pdf/i)) {
2215
                    // PDFs are only displayed if the filesize is smaller than about 1MB (after base64 encoding).
2216
                    // Bigger filesizes currently cause crashes in various browsers.
2217
                    // See also: https://code.google.com/p/chromium/issues/detail?id=69227
2218
2219
                    // Firefox crashes with files that are about 1.5MB
2220
                    // The performance with 1MB files is bearable
2221
                    if (data.length > 1398488) {
2222
                        Alert.showError('File too large, to display a preview. Please download the attachment.'); //TODO: is this error really neccessary?
2223
                        return;
2224
                    }
2225
2226
                    // Fallback for browsers, that don't support the vh unit
2227
                    var clientHeight = $(window).height();
2228
2229
                    $targetElement.html(
2230
                        $(document.createElement('embed'))
2231
                            .attr('src', data)
2232
                            .attr('type', 'application/pdf')
2233
                            .attr('class', 'pdfPreview')
2234
                            .css('height', clientHeight)
2235
                    );
2236
                } else {
2237
                    attachmentHasPreview = false;
2238
                }
2239
            }
2240
        };
2241
2242
        /**
2243
         * attaches the file attachment drag & drop handler to the page
2244
         *
2245
         * @name   AttachmentViewer.addDragDropHandler
2246
         * @private
2247
         * @function
2248
         */
2249
        function addDragDropHandler() {
2250
            if (typeof $fileInput === 'undefined' || $fileInput.length === 0) {
2251
                return;
2252
            }
2253
2254
            var ignoreDragDrop = function(event) {
2255
                event.stopPropagation();
2256
                event.preventDefault();
2257
            };
2258
2259
            var drop = function(event) {
2260
                var evt = event.originalEvent;
2261
                evt.stopPropagation();
2262
                evt.preventDefault();
2263
2264
                if ($fileInput) {
2265
                    var file = evt.dataTransfer.files[0];
2266
                    //Clear the file input:
2267
                    $fileInput.wrap('<form>').closest('form').get(0).reset();
2268
                    $fileInput.unwrap();
2269
                    //Only works in Chrome:
2270
                    //fileInput[0].files = e.dataTransfer.files;
2271
2272
                    readFileData(file);
2273
                }
2274
            };
2275
2276
            $(document).on('drop', drop);
2277
            $(document).on('dragenter', ignoreDragDrop);
2278
            $(document).on('dragover', ignoreDragDrop);
2279
            $fileInput.on('change', function () {
2280
                readFileData();
2281
            });
2282
        }
2283
2284
        /**
2285
         * attaches the clipboard attachment handler to the page
2286
         *
2287
         * @name   AttachmentViewer.addClipboardEventHandler
2288
         * @private
2289
         * @function
2290
         */
2291
        function addClipboardEventHandler() {
2292
            $(document).on('paste', function (event) {
2293
                var items = (event.clipboardData || event.originalEvent.clipboardData).items;
2294
                for (var i in items) {
2295
                    if (items.hasOwnProperty(i)) {
2296
                        var item = items[i];
2297
                        if (item.kind === 'file') {
2298
                            readFileData(item.getAsFile());
2299
                        }
2300
                    }
2301
                }
2302
            });
2303
        }
2304
2305
2306
        /**
2307
         * getter for attachment data
2308
         *
2309
         * @name   AttachmentViewer.getAttachmentData
2310
         * @function
2311
         * @return {jQuery}
2312
         */
2313
        me.getAttachmentData = function () {
2314
            return attachmentData;
2315
        };
2316
2317
        /**
2318
         * getter for attachment link
2319
         *
2320
         * @name   AttachmentViewer.getAttachmentLink
2321
         * @function
2322
         * @return {jQuery}
2323
         */
2324
        me.getAttachmentLink = function () {
2325
            return $attachmentLink;
2326
        };
2327
2328
        /**
2329
         * getter for attachment preview
2330
         *
2331
         * @name   AttachmentViewer.getAttachmentPreview
2332
         * @function
2333
         * @return {jQuery}
2334
         */
2335
        me.getAttachmentPreview = function () {
2336
            return $attachmentPreview;
2337
        };
2338
2339
        /**
2340
         * getter for file data, returns the file contents
2341
         *
2342
         * @name   AttachmentViewer.getFile
2343
         * @function
2344
         * @return {string}
2345
         */
2346
        me.getFile = function () {
2347
            return file;
2348
        };
2349
2350
        /**
2351
         * initiate
2352
         *
2353
         * preloads jQuery elements
2354
         *
2355
         * @name   AttachmentViewer.init
2356
         * @function
2357
         */
2358
        me.init = function()
2359
        {
2360
            $attachment = $('#attachment');
2361
            if($attachment.length){
2362
                $attachmentLink = $('#attachment a');
2363
                $attachmentPreview = $('#attachmentPreview');
2364
                $dragAndDropFileName = $('#dragAndDropFileName');
2365
2366
                $fileInput = $('#file');
2367
                addDragDropHandler();
2368
                addClipboardEventHandler();
2369
            }
2370
        }
2371
2372
        return me;
2373
    })();
2374
2375
    /**
2376
     * (view) Shows discussion thread and handles replies
2377
     *
2378
     * @name   DiscussionViewer
2379
     * @class
2380
     */
2381
    var DiscussionViewer = (function () {
2382
        var me = {};
2383
2384
        var $commentTail,
2385
            $discussion,
2386
            $reply,
2387
            $replyMessage,
2388
            $replyNickname,
2389
            $replyStatus,
2390
            $commentContainer;
2391
2392
        var replyCommentId;
2393
2394
        /**
2395
         * initializes the templates
2396
         *
2397
         * @name   DiscussionViewer.initTemplates
2398
         * @private
2399
         * @function
2400
         */
2401
        function initTemplates()
2402
        {
2403
            $reply = Model.getTemplate('reply');
2404
            $replyMessage = $reply.find('#replymessage');
2405
            $replyNickname = $reply.find('#nickname');
2406
            $replyStatus = $reply.find('#replystatus');
2407
2408
            // cache jQuery elements
2409
            $commentTail = Model.getTemplate('commenttail');
2410
        }
2411
2412
        /**
2413
         * open the comment entry when clicking the "Reply" button of a comment
2414
         *
2415
         * @name   DiscussionViewer.openReply
2416
         * @private
2417
         * @function
2418
         * @param  {Event} event
2419
         */
2420
        function openReply(event)
2421
        {
2422
            var $source = $(event.target);
2423
2424
            // clear input
2425
            $replyMessage.val('');
2426
            $replyNickname.val('');
2427
2428
            // get comment id from source element
2429
            replyCommentId = $source.parent().prop('id').split('_')[1];
2430
2431
            // move to correct position
2432
            $source.after($reply);
2433
2434
            // show
2435
            $reply.removeClass('hidden');
2436
            $replyMessage.focus();
2437
2438
            event.preventDefault();
2439
        }
2440
2441
        /**
2442
         * custom handler for displaying notifications in own status message area
2443
         *
2444
         * @name   DiscussionViewer.handleNotification
2445
         * @function
2446
         * @param  {string} alertType
2447
         * @return {bool|jQuery}
2448
         */
2449
        me.handleNotification = function(alertType)
2450
        {
2451
            // ignore loading messages
2452
            if (alertType === 'loading') {
2453
                return false;
2454
            }
2455
2456
            if (alertType === 'danger') {
2457
                $replyStatus.removeClass('alert-info');
2458
                $replyStatus.addClass('alert-danger');
2459
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2460
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2461
            } else {
2462
                $replyStatus.removeClass('alert-danger');
2463
                $replyStatus.addClass('alert-info');
2464
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2465
                $replyStatus.find(':first').addClass('glyphicon-alert');
2466
            }
2467
2468
            return $replyStatus;
2469
        };
2470
2471
        /**
2472
         * adds another comment
2473
         *
2474
         * @name   DiscussionViewer.addComment
2475
         * @function
2476
         * @param {object} comment
2477
         * @param {string} commentText
2478
         * @param {string} nickname
2479
         */
2480
        me.addComment = function(comment, commentText, nickname)
2481
        {
2482
            if (commentText === '') {
2483
                commentText = 'comment decryption failed';
2484
            }
2485
2486
            // create new comment based on template
2487
            var $commentEntry = Model.getTemplate('comment');
2488
            $commentEntry.prop('id', 'comment_' + comment.id);
2489
            var $commentEntryData = $commentEntry.find('div.commentdata');
2490
2491
            // set & parse text
2492
            $commentEntryData.html(
2493
                DOMPurify.sanitize(
2494
                    Helper.urls2links(commentText)
2495
                )
2496
            );
2497
2498
            // set nickname
2499
            if (nickname.length > 0) {
2500
                $commentEntry.find('span.nickname').text(nickname);
2501
            } else {
2502
                $commentEntry.find('span.nickname').html('<i></i>');
2503
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2504
            }
2505
2506
            // set date
2507
            $commentEntry.find('span.commentdate')
2508
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2509
                      .attr('title', 'CommentID: ' + comment.id);
2510
2511
            // if an avatar is available, display it
2512
            if (comment.meta.vizhash) {
2513
                $commentEntry.find('span.nickname')
2514
                             .before(
2515
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2516
                             );
2517
                $(document).on('languageLoaded', function () {
2518
                    $commentEntry.find('img.vizhash')
2519
                                 .prop('title', I18n._('Avatar generated from IP address'));
2520
                });
2521
            }
2522
2523
            // starting point (default value/fallback)
2524
            var $place = $commentContainer;
2525
2526
            // if parent comment exists
2527
            var $parentComment = $('#comment_' + comment.parentid);
2528
            if ($parentComment.length) {
2529
                // use parent as position for new comment, so it is shifted
2530
                // to the right
2531
                $place = $parentComment;
2532
            }
2533
2534
            // finally append comment
2535
            $place.append($commentEntry);
2536
        };
2537
2538
        /**
2539
         * finishes the discussion area after last comment
2540
         *
2541
         * @name   DiscussionViewer.finishDiscussion
2542
         * @function
2543
         */
2544
        me.finishDiscussion = function()
2545
        {
2546
            // add 'add new comment' area
2547
            $commentContainer.append($commentTail);
2548
2549
            // show discussions
2550
            $discussion.removeClass('hidden');
2551
        };
2552
2553
        /**
2554
         * removes the old discussion and prepares everything for creating a new
2555
         * one.
2556
         *
2557
         * @name   DiscussionViewer.prepareNewDiscussion
2558
         * @function
2559
         */
2560
        me.prepareNewDiscussion = function()
2561
        {
2562
            $commentContainer.html('');
2563
            $discussion.addClass('hidden');
2564
2565
            // (re-)init templates
2566
            initTemplates();
2567
        };
2568
2569
        /**
2570
         * returns the users message from the reply form
2571
         *
2572
         * @name   DiscussionViewer.getReplyMessage
2573
         * @function
2574
         * @return {String}
2575
         */
2576
        me.getReplyMessage = function()
2577
        {
2578
            return $replyMessage.val();
2579
        };
2580
2581
        /**
2582
         * returns the users nickname (if any) from the reply form
2583
         *
2584
         * @name   DiscussionViewer.getReplyNickname
2585
         * @function
2586
         * @return {String}
2587
         */
2588
        me.getReplyNickname = function()
2589
        {
2590
            return $replyNickname.val();
2591
        };
2592
2593
        /**
2594
         * returns the id of the parent comment the user is replying to
2595
         *
2596
         * @name   DiscussionViewer.getReplyCommentId
2597
         * @function
2598
         * @return {int|undefined}
2599
         */
2600
        me.getReplyCommentId = function()
2601
        {
2602
            return replyCommentId;
2603
        };
2604
2605
        /**
2606
         * highlights a specific comment and scrolls to it if necessary
2607
         *
2608
         * @name   DiscussionViewer.highlightComment
2609
         * @function
2610
         * @param {string} commentId
2611
         * @param {bool} fadeOut - whether to fade out the comment
2612
         */
2613
        me.highlightComment = function(commentId, fadeOut)
2614
        {
2615
            var $comment = $('#comment_' + commentId);
2616
            // in case comment does not exist, cancel
2617
            if ($comment.length === 0) {
2618
                return;
2619
            }
2620
2621
            var highlightComment = function () {
2622
                $comment.addClass('highlight');
2623
                if (fadeOut === true) {
2624
                    setTimeout(function () {
2625
                        $comment.removeClass('highlight');
2626
                    }, 300);
2627
                }
2628
            };
2629
2630
            if (UiHelper.isVisible($comment)) {
2631
                return highlightComment();
2632
            }
2633
2634
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2635
        };
2636
2637
        /**
2638
         * initiate
2639
         *
2640
         * preloads jQuery elements
2641
         *
2642
         * @name   DiscussionViewer.init
2643
         * @function
2644
         */
2645
        me.init = function()
2646
        {
2647
            // bind events to templates (so they are later cloned)
2648
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2649
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2650
2651
            $commentContainer = $('#commentcontainer');
2652
            $discussion = $('#discussion');
2653
        };
2654
2655
        return me;
2656
    })();
2657
2658
    /**
2659
     * Manage top (navigation) bar
2660
     *
2661
     * @name   TopNav
2662
     * @param  {object} window
2663
     * @param  {object} document
2664
     * @class
2665
     */
2666
    var TopNav = (function (window, document) {
2667
        var me = {};
2668
2669
        var createButtonsDisplayed = false;
2670
        var viewButtonsDisplayed = false;
2671
2672
        var $attach,
2673
            $burnAfterReading,
2674
            $burnAfterReadingOption,
2675
            $cloneButton,
2676
            $customAttachment,
2677
            $expiration,
2678
            $fileRemoveButton,
2679
            $fileWrap,
2680
            $formatter,
2681
            $newButton,
2682
            $openDiscussion,
2683
            $openDiscussionOption,
2684
            $password,
2685
            $passwordInput,
2686
            $rawTextButton,
2687
            $qrCodeLink,
2688
            $sendButton;
2689
2690
        var pasteExpiration = '1week';
2691
2692
        /**
2693
         * set the expiration on bootstrap templates in dropdown
2694
         *
2695
         * @name   TopNav.updateExpiration
2696
         * @private
2697
         * @function
2698
         * @param  {Event} event
2699
         */
2700
        function updateExpiration(event)
2701
        {
2702
            // get selected option
2703
            var target = $(event.target);
2704
2705
            // update dropdown display and save new expiration time
2706
            $('#pasteExpirationDisplay').text(target.text());
2707
            pasteExpiration = target.data('expiration');
2708
2709
            event.preventDefault();
2710
        }
2711
2712
        /**
2713
         * set the format on bootstrap templates in dropdown
2714
         *
2715
         * @name   TopNav.updateFormat
2716
         * @private
2717
         * @function
2718
         * @param  {Event} event
2719
         */
2720
        function updateFormat(event)
2721
        {
2722
            // get selected option
2723
            var $target = $(event.target);
2724
2725
            // update dropdown display and save new format
2726
            var newFormat = $target.data('format');
2727
            $('#pasteFormatterDisplay').text($target.text());
2728
            PasteViewer.setFormat(newFormat);
2729
2730
            // update preview
2731
            if (Editor.isPreview()) {
2732
                PasteViewer.run();
2733
            }
2734
2735
            event.preventDefault();
2736
        }
2737
2738
        /**
2739
         * when "burn after reading" is checked, disable discussion
2740
         *
2741
         * @name   TopNav.changeBurnAfterReading
2742
         * @private
2743
         * @function
2744
         */
2745
        function changeBurnAfterReading()
2746
        {
2747
            if ($burnAfterReading.is(':checked')) {
2748
                $openDiscussionOption.addClass('buttondisabled');
2749
                $openDiscussion.prop('checked', false);
2750
2751
                // if button is actually disabled, force-enable it and uncheck other button
2752
                $burnAfterReadingOption.removeClass('buttondisabled');
2753
            } else {
2754
                $openDiscussionOption.removeClass('buttondisabled');
2755
            }
2756
        }
2757
2758
        /**
2759
         * when discussion is checked, disable "burn after reading"
2760
         *
2761
         * @name   TopNav.changeOpenDiscussion
2762
         * @private
2763
         * @function
2764
         */
2765
        function changeOpenDiscussion()
2766
        {
2767
            if ($openDiscussion.is(':checked')) {
2768
                $burnAfterReadingOption.addClass('buttondisabled');
2769
                $burnAfterReading.prop('checked', false);
2770
2771
                // if button is actually disabled, force-enable it and uncheck other button
2772
                $openDiscussionOption.removeClass('buttondisabled');
2773
            } else {
2774
                $burnAfterReadingOption.removeClass('buttondisabled');
2775
            }
2776
        }
2777
2778
        /**
2779
         * return raw text
2780
         *
2781
         * @name   TopNav.rawText
2782
         * @private
2783
         * @function
2784
         */
2785
        function rawText()
2786
        {
2787
            TopNav.hideAllButtons();
2788
            Alert.showLoading('Showing raw text…', 'time');
2789
            var paste = PasteViewer.getText();
2790
2791
            // push a new state to allow back navigation with browser back button
2792
            history.pushState(
2793
                {type: 'raw'},
2794
                document.title,
2795
                // recreate paste URL
2796
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2797
                Model.getPasteKey()
2798
            );
2799
2800
            // we use text/html instead of text/plain to avoid a bug when
2801
            // reloading the raw text view (it reverts to type text/html)
2802
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2803
            var newDoc = document.open('text/html', 'replace');
2804
            newDoc.write('<!DOCTYPE html><html><head>');
2805
            for (var i = 0; i < $head.length; i++) {
2806
                newDoc.write($head[i].outerHTML);
2807
            }
2808
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2809
            newDoc.close();
2810
        }
2811
2812
        /**
2813
         * saves the language in a cookie and reloads the page
2814
         *
2815
         * @name   TopNav.setLanguage
2816
         * @private
2817
         * @function
2818
         * @param  {Event} event
2819
         */
2820
        function setLanguage(event)
2821
        {
2822
            document.cookie = 'lang=' + $(event.target).data('lang');
2823
            UiHelper.reloadHome();
2824
        }
2825
2826
        /**
2827
         * hides all messages and creates a new paste
2828
         *
2829
         * @name   TopNav.clickNewPaste
2830
         * @private
2831
         * @function
2832
         */
2833
        function clickNewPaste()
2834
        {
2835
            Controller.hideStatusMessages();
2836
            Controller.newPaste();
2837
        }
2838
2839
        /**
2840
         * removes the existing attachment
2841
         *
2842
         * @name   TopNav.removeAttachment
2843
         * @private
2844
         * @function
2845
         * @param  {Event} event
2846
         */
2847
        function removeAttachment(event)
2848
        {
2849
            // if custom attachment is used, remove it first
2850
            if (!$customAttachment.hasClass('hidden')) {
2851
                AttachmentViewer.removeAttachment();
2852
                $customAttachment.addClass('hidden');
2853
                $fileWrap.removeClass('hidden');
2854
            }
2855
2856
            // in any case, remove saved attachment data
2857
            AttachmentViewer.removeAttachmentData();
2858
2859
            // hide UI for selected files
2860
            // our up-to-date jQuery can handle it :)
2861
            $fileWrap.find('input').val('');
2862
            AttachmentViewer.clearDragAndDrop();
2863
2864
            // pevent '#' from appearing in the URL
2865
            event.preventDefault();
2866
        }
2867
2868
        /**
2869
         * Shows the QR code of the current paste (URL).
2870
         *
2871
         * @name   TopNav.displayQrCode
2872
         * @private
2873
         * @function
2874
         */
2875
        function displayQrCode()
2876
        {
2877
            var qrCanvas = kjua({
2878
                render: 'canvas',
2879
                text: window.location.href
2880
            });
2881
            $('#qrcode-display').html(qrCanvas);
2882
        }
2883
2884
        /**
2885
         * Shows all navigation elements for viewing an existing paste
2886
         *
2887
         * @name   TopNav.showViewButtons
2888
         * @function
2889
         */
2890
        me.showViewButtons = function()
2891
        {
2892
            if (viewButtonsDisplayed) {
2893
                console.warn('showViewButtons: view buttons are already displayed');
2894
                return;
2895
            }
2896
2897
            $newButton.removeClass('hidden');
2898
            $cloneButton.removeClass('hidden');
2899
            $rawTextButton.removeClass('hidden');
2900
            $qrCodeLink.removeClass('hidden');
2901
2902
            viewButtonsDisplayed = true;
2903
        };
2904
2905
        /**
2906
         * Hides all navigation elements for viewing an existing paste
2907
         *
2908
         * @name   TopNav.hideViewButtons
2909
         * @function
2910
         */
2911
        me.hideViewButtons = function()
2912
        {
2913
            if (!viewButtonsDisplayed) {
2914
                console.warn('hideViewButtons: view buttons are already hidden');
2915
                return;
2916
            }
2917
2918
            $newButton.addClass('hidden');
2919
            $cloneButton.addClass('hidden');
2920
            $rawTextButton.addClass('hidden');
2921
            $qrCodeLink.addClass('hidden');
2922
2923
            viewButtonsDisplayed = false;
2924
        };
2925
2926
        /**
2927
         * Hides all elements belonging to existing pastes
2928
         *
2929
         * @name   TopNav.hideAllButtons
2930
         * @function
2931
         */
2932
        me.hideAllButtons = function()
2933
        {
2934
            me.hideViewButtons();
2935
            me.hideCreateButtons();
2936
        };
2937
2938
        /**
2939
         * shows all elements needed when creating a new paste
2940
         *
2941
         * @name   TopNav.showCreateButtons
2942
         * @function
2943
         */
2944
        me.showCreateButtons = function()
2945
        {
2946
            if (createButtonsDisplayed) {
2947
                console.warn('showCreateButtons: create buttons are already displayed');
2948
                return;
2949
            }
2950
2951
            $sendButton.removeClass('hidden');
2952
            $expiration.removeClass('hidden');
2953
            $formatter.removeClass('hidden');
2954
            $burnAfterReadingOption.removeClass('hidden');
2955
            $openDiscussionOption.removeClass('hidden');
2956
            $newButton.removeClass('hidden');
2957
            $password.removeClass('hidden');
2958
            $attach.removeClass('hidden');
2959
2960
            createButtonsDisplayed = true;
2961
        };
2962
2963
        /**
2964
         * shows all elements needed when creating a new paste
2965
         *
2966
         * @name   TopNav.hideCreateButtons
2967
         * @function
2968
         */
2969
        me.hideCreateButtons = function()
2970
        {
2971
            if (!createButtonsDisplayed) {
2972
                console.warn('hideCreateButtons: create buttons are already hidden');
2973
                return;
2974
            }
2975
2976
            $newButton.addClass('hidden');
2977
            $sendButton.addClass('hidden');
2978
            $expiration.addClass('hidden');
2979
            $formatter.addClass('hidden');
2980
            $burnAfterReadingOption.addClass('hidden');
2981
            $openDiscussionOption.addClass('hidden');
2982
            $password.addClass('hidden');
2983
            $attach.addClass('hidden');
2984
2985
            createButtonsDisplayed = false;
2986
        };
2987
2988
        /**
2989
         * only shows the "new paste" button
2990
         *
2991
         * @name   TopNav.showNewPasteButton
2992
         * @function
2993
         */
2994
        me.showNewPasteButton = function()
2995
        {
2996
            $newButton.removeClass('hidden');
2997
        };
2998
2999
        /**
3000
         * only hides the clone button
3001
         *
3002
         * @name   TopNav.hideCloneButton
3003
         * @function
3004
         */
3005
        me.hideCloneButton = function()
3006
        {
3007
            $cloneButton.addClass('hidden');
3008
        };
3009
3010
        /**
3011
         * only hides the raw text button
3012
         *
3013
         * @name   TopNav.hideRawButton
3014
         * @function
3015
         */
3016
        me.hideRawButton = function()
3017
        {
3018
            $rawTextButton.addClass('hidden');
3019
        };
3020
3021
        /**
3022
         * hides the file selector in attachment
3023
         *
3024
         * @name   TopNav.hideFileSelector
3025
         * @function
3026
         */
3027
        me.hideFileSelector = function()
3028
        {
3029
            $fileWrap.addClass('hidden');
3030
        };
3031
3032
3033
        /**
3034
         * shows the custom attachment
3035
         *
3036
         * @name   TopNav.showCustomAttachment
3037
         * @function
3038
         */
3039
        me.showCustomAttachment = function()
3040
        {
3041
            $customAttachment.removeClass('hidden');
3042
        };
3043
3044
        /**
3045
         * collapses the navigation bar, only if expanded
3046
         *
3047
         * @name   TopNav.collapseBar
3048
         * @function
3049
         */
3050
        me.collapseBar = function()
3051
        {
3052
            if ($('#navbar').attr('aria-expanded') === 'true') {
3053
                $('.navbar-toggle').click();
3054
            }
3055
        };
3056
3057
        /**
3058
         * returns the currently set expiration time
3059
         *
3060
         * @name   TopNav.getExpiration
3061
         * @function
3062
         * @return {int}
3063
         */
3064
        me.getExpiration = function()
3065
        {
3066
            return pasteExpiration;
3067
        };
3068
3069
        /**
3070
         * returns the currently selected file(s)
3071
         *
3072
         * @name   TopNav.getFileList
3073
         * @function
3074
         * @return {FileList|null}
3075
         */
3076
        me.getFileList = function()
3077
        {
3078
            var $file = $('#file');
3079
3080
            // if no file given, return null
3081
            if (!$file.length || !$file[0].files.length) {
3082
                return null;
3083
            }
3084
3085
            // ensure the selected file is still accessible
3086
            if (!($file[0].files && $file[0].files[0])) {
3087
                return null;
3088
            }
3089
3090
            return $file[0].files;
3091
        };
3092
3093
        /**
3094
         * returns the state of the burn after reading checkbox
3095
         *
3096
         * @name   TopNav.getExpiration
3097
         * @function
3098
         * @return {bool}
3099
         */
3100
        me.getBurnAfterReading = function()
3101
        {
3102
            return $burnAfterReading.is(':checked');
3103
        };
3104
3105
        /**
3106
         * returns the state of the discussion checkbox
3107
         *
3108
         * @name   TopNav.getOpenDiscussion
3109
         * @function
3110
         * @return {bool}
3111
         */
3112
        me.getOpenDiscussion = function()
3113
        {
3114
            return $openDiscussion.is(':checked');
3115
        };
3116
3117
        /**
3118
         * returns the entered password
3119
         *
3120
         * @name   TopNav.getPassword
3121
         * @function
3122
         * @return {string}
3123
         */
3124
        me.getPassword = function()
3125
        {
3126
            return $passwordInput.val();
3127
        };
3128
3129
        /**
3130
         * returns the element where custom attachments can be placed
3131
         *
3132
         * Used by AttachmentViewer when an attachment is cloned here.
3133
         *
3134
         * @name   TopNav.getCustomAttachment
3135
         * @function
3136
         * @return {jQuery}
3137
         */
3138
        me.getCustomAttachment = function()
3139
        {
3140
            return $customAttachment;
3141
        };
3142
3143
        /**
3144
         * init navigation manager
3145
         *
3146
         * preloads jQuery elements
3147
         *
3148
         * @name   TopNav.init
3149
         * @function
3150
         */
3151
        me.init = function()
3152
        {
3153
            $attach = $('#attach');
3154
            $burnAfterReading = $('#burnafterreading');
3155
            $burnAfterReadingOption = $('#burnafterreadingoption');
3156
            $cloneButton = $('#clonebutton');
3157
            $customAttachment = $('#customattachment');
3158
            $expiration = $('#expiration');
3159
            $fileRemoveButton = $('#fileremovebutton');
3160
            $fileWrap = $('#filewrap');
3161
            $formatter = $('#formatter');
3162
            $newButton = $('#newbutton');
3163
            $openDiscussion = $('#opendiscussion');
3164
            $openDiscussionOption = $('#opendiscussionoption');
3165
            $password = $('#password');
3166
            $passwordInput = $('#passwordinput');
3167
            $rawTextButton = $('#rawtextbutton');
3168
            $sendButton = $('#sendbutton');
3169
            $qrCodeLink = $('#qrcodelink');
3170
3171
            // bootstrap template drop down
3172
            $('#language ul.dropdown-menu li a').click(setLanguage);
3173
            // page template drop down
3174
            $('#language select option').click(setLanguage);
3175
3176
            // bind events
3177
            $burnAfterReading.change(changeBurnAfterReading);
3178
            $openDiscussionOption.change(changeOpenDiscussion);
3179
            $newButton.click(clickNewPaste);
3180
            $sendButton.click(PasteEncrypter.sendPaste);
3181
            $cloneButton.click(Controller.clonePaste);
3182
            $rawTextButton.click(rawText);
3183
            $fileRemoveButton.click(removeAttachment);
3184
            $qrCodeLink.click(displayQrCode);
3185
3186
            // bootstrap template drop downs
3187
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
3188
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
3189
3190
            // initiate default state of checkboxes
3191
            changeBurnAfterReading();
3192
            changeOpenDiscussion();
3193
3194
            // get default value from template or fall back to set value
3195
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
3196
3197
            createButtonsDisplayed = false;
3198
            viewButtonsDisplayed = false;
3199
        };
3200
3201
        return me;
3202
    })(window, document);
3203
3204
    /**
3205
     * Responsible for AJAX requests, transparently handles encryption…
3206
     *
3207
     * @name   Uploader
3208
     * @class
3209
     */
3210
    var Uploader = (function () {
3211
        var me = {};
3212
3213
        var successFunc = null,
3214
            failureFunc = null,
3215
            url,
3216
            data,
3217
            symmetricKey,
3218
            password;
3219
3220
        /**
3221
         * public variable ('constant') for errors to prevent magic numbers
3222
         *
3223
         * @name   Uploader.error
3224
         * @readonly
3225
         * @enum   {Object}
3226
         */
3227
        me.error = {
3228
            okay: 0,
3229
            custom: 1,
3230
            unknown: 2,
3231
            serverError: 3
3232
        };
3233
3234
        /**
3235
         * ajaxHeaders to send in AJAX requests
3236
         *
3237
         * @name   Uploader.ajaxHeaders
3238
         * @private
3239
         * @readonly
3240
         * @enum   {Object}
3241
         */
3242
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
3243
3244
        /**
3245
         * called after successful upload
3246
         *
3247
         * @name   Uploader.checkCryptParameters
3248
         * @private
3249
         * @function
3250
         * @throws {string}
3251
         */
3252
        function checkCryptParameters()
3253
        {
3254
            // workaround for this nasty 'bug' in ECMAScript
3255
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3256
            var typeOfKey = typeof symmetricKey;
3257
            if (symmetricKey === null) {
3258
                typeOfKey = 'null';
3259
            }
3260
3261
            // in case of missing preparation, throw error
3262
            switch (typeOfKey) {
3263
                case 'string':
3264
                    // already set, all right
3265
                    return;
3266
                case 'null':
3267
                    // needs to be generated auto-generate
3268
                    symmetricKey = CryptTool.getSymmetricKey();
3269
                    break;
3270
                default:
3271
                    console.error('current invalid symmetricKey:', symmetricKey);
3272
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3273
            }
3274
            // password is optional
3275
        }
3276
3277
        /**
3278
         * called after successful upload
3279
         *
3280
         * @name   Uploader.success
3281
         * @private
3282
         * @function
3283
         * @param {int} status
3284
         * @param {int} result - optional
3285
         */
3286
        function success(status, result)
3287
        {
3288
            // add useful data to result
3289
            result.encryptionKey = symmetricKey;
3290
            result.requestData = data;
3291
3292
            if (successFunc !== null) {
3293
                successFunc(status, result);
3294
            }
3295
        }
3296
3297
        /**
3298
         * called after a upload failure
3299
         *
3300
         * @name   Uploader.fail
3301
         * @private
3302
         * @function
3303
         * @param {int} status - internal code
3304
         * @param {int} result - original error code
3305
         */
3306
        function fail(status, result)
3307
        {
3308
            if (failureFunc !== null) {
3309
                failureFunc(status, result);
3310
            }
3311
        }
3312
3313
        /**
3314
         * actually uploads the data
3315
         *
3316
         * @name   Uploader.run
3317
         * @function
3318
         */
3319
        me.run = function()
3320
        {
3321
            $.ajax({
3322
                type: 'POST',
3323
                url: url,
3324
                data: data,
3325
                dataType: 'json',
3326
                headers: ajaxHeaders,
3327
                success: function(result) {
3328
                    if (result.status === 0) {
3329
                        success(0, result);
3330
                    } else if (result.status === 1) {
3331
                        fail(1, result);
3332
                    } else {
3333
                        fail(2, result);
3334
                    }
3335
                }
3336
            })
3337
            .fail(function(jqXHR, textStatus, errorThrown) {
3338
                console.error(textStatus, errorThrown);
3339
                fail(3, jqXHR);
3340
            });
3341
        };
3342
3343
        /**
3344
         * set success function
3345
         *
3346
         * @name   Uploader.setUrl
3347
         * @function
3348
         * @param {function} newUrl
3349
         */
3350
        me.setUrl = function(newUrl)
3351
        {
3352
            url = newUrl;
3353
        };
3354
3355
        /**
3356
         * sets the password to use (first value) and optionally also the
3357
         * encryption key (not recommend, it is automatically generated).
3358
         *
3359
         * Note: Call this after prepare() as prepare() resets these values.
3360
         *
3361
         * @name   Uploader.setCryptValues
3362
         * @function
3363
         * @param {string} newPassword
3364
         * @param {string} newKey       - optional
3365
         */
3366
        me.setCryptParameters = function(newPassword, newKey)
3367
        {
3368
            password = newPassword;
3369
3370
            if (typeof newKey !== 'undefined') {
3371
                symmetricKey = newKey;
3372
            }
3373
        };
3374
3375
        /**
3376
         * set success function
3377
         *
3378
         * @name   Uploader.setSuccess
3379
         * @function
3380
         * @param {function} func
3381
         */
3382
        me.setSuccess = function(func)
3383
        {
3384
            successFunc = func;
3385
        };
3386
3387
        /**
3388
         * set failure function
3389
         *
3390
         * @name   Uploader.setFailure
3391
         * @function
3392
         * @param {function} func
3393
         */
3394
        me.setFailure = function(func)
3395
        {
3396
            failureFunc = func;
3397
        };
3398
3399
        /**
3400
         * prepares a new upload
3401
         *
3402
         * Call this when doing a new upload to reset any data from potential
3403
         * previous uploads. Must be called before any other method of this
3404
         * module.
3405
         *
3406
         * @name   Uploader.prepare
3407
         * @function
3408
         * @return {object}
3409
         */
3410
        me.prepare = function()
3411
        {
3412
            // entropy should already be checked!
3413
3414
            // reset password
3415
            password = '';
3416
3417
            // reset key, so it a new one is generated when it is used
3418
            symmetricKey = null;
3419
3420
            // reset data
3421
            successFunc = null;
3422
            failureFunc = null;
3423
            url = Helper.baseUri();
3424
            data = {};
3425
        };
3426
3427
        /**
3428
         * encrypts and sets the data
3429
         *
3430
         * @name   Uploader.setData
3431
         * @function
3432
         * @param {string} index
3433
         * @param {mixed} element
3434
         */
3435
        me.setData = function(index, element)
3436
        {
3437
            checkCryptParameters();
3438
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3439
        };
3440
3441
        /**
3442
         * set the additional metadata to send unencrypted
3443
         *
3444
         * @name   Uploader.setUnencryptedData
3445
         * @function
3446
         * @param {string} index
3447
         * @param {mixed} element
3448
         */
3449
        me.setUnencryptedData = function(index, element)
3450
        {
3451
            data[index] = element;
3452
        };
3453
3454
        /**
3455
         * set the additional metadata to send unencrypted passed at once
3456
         *
3457
         * @name   Uploader.setUnencryptedData
3458
         * @function
3459
         * @param {object} newData
3460
         */
3461
        me.setUnencryptedBulkData = function(newData)
3462
        {
3463
            $.extend(data, newData);
3464
        };
3465
3466
        /**
3467
         * Helper, which parses shows a general error message based on the result of the Uploader
3468
         *
3469
         * @name    Uploader.parseUploadError
3470
         * @function
3471
         * @param {int} status
3472
         * @param {object} data
3473
         * @param {string} doThisThing - a human description of the action, which was tried
3474
         * @return {array}
3475
         */
3476
        me.parseUploadError = function(status, data, doThisThing) {
3477
            var errorArray;
3478
3479
            switch (status) {
3480
                case me.error.custom:
3481
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3482
                    break;
3483
                case me.error.unknown:
3484
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3485
                    break;
3486
                case me.error.serverError:
3487
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3488
                    break;
3489
                default:
3490
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3491
                    break;
3492
            }
3493
3494
            return errorArray;
3495
        };
3496
3497
        /**
3498
         * init Uploader
3499
         *
3500
         * @name   Uploader.init
3501
         * @function
3502
         */
3503
        me.init = function()
3504
        {
3505
            // nothing yet
3506
        };
3507
3508
        return me;
3509
    })();
3510
3511
    /**
3512
     * (controller) Responsible for encrypting paste and sending it to server.
3513
     *
3514
     * Does upload, encryption is done transparently by Uploader.
3515
     *
3516
     * @name PasteEncrypter
3517
     * @class
3518
     */
3519
    var PasteEncrypter = (function () {
3520
        var me = {};
3521
3522
        var requirementsChecked = false;
3523
3524
        /**
3525
         * checks whether there is a suitable amount of entrophy
3526
         *
3527
         * @name PasteEncrypter.checkRequirements
3528
         * @private
3529
         * @function
3530
         * @param {function} retryCallback - the callback to execute to retry the upload
3531
         * @return {bool}
3532
         */
3533
        function checkRequirements(retryCallback) {
3534
            // skip double requirement checks
3535
            if (requirementsChecked === true) {
3536
                return true;
3537
            }
3538
3539
            if (!CryptTool.isEntropyReady()) {
3540
                // display a message and wait
3541
                Alert.showStatus('Please move your mouse for more entropy…');
3542
3543
                CryptTool.addEntropySeedListener(retryCallback);
3544
                return false;
3545
            }
3546
3547
            requirementsChecked = true;
3548
3549
            return true;
3550
        }
3551
3552
        /**
3553
         * called after successful paste upload
3554
         *
3555
         * @name PasteEncrypter.showCreatedPaste
3556
         * @private
3557
         * @function
3558
         * @param {int} status
3559
         * @param {object} data
3560
         */
3561
        function showCreatedPaste(status, data) {
3562
            Alert.hideLoading();
3563
3564
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3565
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3566
3567
            Alert.hideMessages();
3568
3569
            // show notification
3570
            PasteStatus.createPasteNotification(url, deleteUrl);
3571
3572
            // show new URL in browser bar
3573
            history.pushState({type: 'newpaste'}, document.title, url);
3574
3575
            TopNav.showViewButtons();
3576
            TopNav.hideRawButton();
3577
            Editor.hide();
3578
3579
            // parse and show text
3580
            // (preparation already done in me.sendPaste())
3581
            PasteViewer.run();
3582
        }
3583
3584
        /**
3585
         * called after successful comment upload
3586
         *
3587
         * @name PasteEncrypter.showUploadedComment
3588
         * @private
3589
         * @function
3590
         * @param {int} status
3591
         * @param {object} data
3592
         */
3593
        function showUploadedComment(status, data) {
3594
            // show success message
3595
            Alert.showStatus('Comment posted.');
3596
3597
            // reload paste
3598
            Controller.refreshPaste(function () {
3599
                // highlight sent comment
3600
                DiscussionViewer.highlightComment(data.id, true);
3601
                // reset error handler
3602
                Alert.setCustomHandler(null);
3603
            });
3604
        }
3605
3606
        /**
3607
         * adds attachments to the Uploader
3608
         *
3609
         * @name PasteEncrypter.encryptAttachments
3610
         * @private
3611
         * @function
3612
         * @param {function} callback - excuted when action is successful
3613
         */
3614
        function encryptAttachments(callback) {
3615
            var file = AttachmentViewer.getAttachmentData();
3616
3617
            if (typeof file !== 'undefined' && file !== null) {
3618
                var fileName = AttachmentViewer.getFile().name;
3619
3620
                Uploader.setData('attachment', file);
3621
                Uploader.setData('attachmentname', fileName);
3622
3623
                // run callback
3624
                return callback();
3625
            } else if (AttachmentViewer.hasAttachment()) {
3626
                // fall back to cloned part
3627
                var attachment = AttachmentViewer.getAttachment();
3628
3629
                Uploader.setData('attachment', attachment[0]);
3630
                Uploader.setData('attachmentname', attachment[1]);
3631
                return callback();
3632
            } else {
3633
                // if there are no attachments, this is of course still successful
3634
                return callback();
3635
            }
3636
        }
3637
3638
        /**
3639
         * send a reply in a discussion
3640
         *
3641
         * @name   PasteEncrypter.sendComment
3642
         * @function
3643
         */
3644
        me.sendComment = function()
3645
        {
3646
            Alert.hideMessages();
3647
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3648
3649
            // UI loading state
3650
            TopNav.hideAllButtons();
3651
            Alert.showLoading('Sending comment…', 'cloud-upload');
3652
3653
            // get data
3654
            var plainText = DiscussionViewer.getReplyMessage(),
3655
                nickname = DiscussionViewer.getReplyNickname(),
3656
                parentid = DiscussionViewer.getReplyCommentId();
3657
3658
            // do not send if there is no data
3659
            if (plainText.length === 0) {
3660
                // revert loading status…
3661
                Alert.hideLoading();
3662
                Alert.setCustomHandler(null);
3663
                TopNav.showViewButtons();
3664
                return;
3665
            }
3666
3667
            // check entropy
3668
            if (!checkRequirements(function () {
3669
                me.sendComment();
3670
            })) {
3671
                return; // to prevent multiple executions
3672
            }
3673
3674
            // prepare Uploader
3675
            Uploader.prepare();
3676
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3677
3678
            // set success/fail functions
3679
            Uploader.setSuccess(showUploadedComment);
3680
            Uploader.setFailure(function (status, data) {
3681
                // revert loading status…
3682
                Alert.hideLoading();
3683
                TopNav.showViewButtons();
3684
3685
                // show error message
3686
                Alert.showError(
3687
                    Uploader.parseUploadError(status, data, 'post comment')
3688
                );
3689
3690
                // reset error handler
3691
                Alert.setCustomHandler(null);
3692
            });
3693
3694
            // fill it with unencrypted params
3695
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3696
            if (typeof parentid === 'undefined') {
3697
                // if parent id is not set, this is the top-most comment, so use
3698
                // paste id as parent, as the root element of the discussion tree
3699
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3700
            } else {
3701
                Uploader.setUnencryptedData('parentid', parentid);
3702
            }
3703
3704
            // encrypt data
3705
            Uploader.setData('data', plainText);
3706
3707
            if (nickname.length > 0) {
3708
                Uploader.setData('nickname', nickname);
3709
            }
3710
3711
            Uploader.run();
3712
        };
3713
3714
        /**
3715
         * sends a new paste to server
3716
         *
3717
         * @name   PasteEncrypter.sendPaste
3718
         * @function
3719
         */
3720
        me.sendPaste = function()
3721
        {
3722
            // hide previous (error) messages
3723
            Controller.hideStatusMessages();
3724
3725
            // UI loading state
3726
            TopNav.hideAllButtons();
3727
            Alert.showLoading('Sending paste…', 'cloud-upload');
3728
            TopNav.collapseBar();
3729
3730
            // get data
3731
            var plainText = Editor.getText(),
3732
                format = PasteViewer.getFormat(),
3733
                // the methods may return different values if no files are attached (null, undefined or false)
3734
                files = TopNav.getFileList() || AttachmentViewer.getFile() || AttachmentViewer.hasAttachment();
3735
3736
            // do not send if there is no data
3737
            if (plainText.length === 0 && !files) {
3738
                // revert loading status…
3739
                Alert.hideLoading();
3740
                TopNav.showCreateButtons();
3741
                return;
3742
            }
3743
3744
            // check entropy
3745
            if (!checkRequirements(function () {
3746
                me.sendPaste();
3747
            })) {
3748
                return; // to prevent multiple executions
3749
            }
3750
3751
            // prepare Uploader
3752
            Uploader.prepare();
3753
            Uploader.setCryptParameters(TopNav.getPassword());
3754
3755
            // set success/fail functions
3756
            Uploader.setSuccess(showCreatedPaste);
3757
            Uploader.setFailure(function (status, data) {
3758
                // revert loading status…
3759
                Alert.hideLoading();
3760
                TopNav.showCreateButtons();
3761
3762
                // show error message
3763
                Alert.showError(
3764
                    Uploader.parseUploadError(status, data, 'create paste')
3765
                );
3766
            });
3767
3768
            // fill it with unencrypted submitted options
3769
            Uploader.setUnencryptedBulkData({
3770
                expire:           TopNav.getExpiration(),
3771
                formatter:        format,
3772
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3773
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3774
            });
3775
3776
            // prepare PasteViewer for later preview
3777
            PasteViewer.setText(plainText);
3778
            PasteViewer.setFormat(format);
3779
3780
            // encrypt cipher data
3781
            Uploader.setData('data', plainText);
3782
3783
            // encrypt attachments
3784
            encryptAttachments(
3785
                function () {
3786
                    // send data
3787
                    Uploader.run();
3788
                }
3789
            );
3790
        };
3791
3792
        /**
3793
         * initialize
3794
         *
3795
         * @name   PasteEncrypter.init
3796
         * @function
3797
         */
3798
        me.init = function()
3799
        {
3800
            // nothing yet
3801
        };
3802
3803
        return me;
3804
    })();
3805
3806
    /**
3807
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3808
     *
3809
     * Only decryption, no download.
3810
     *
3811
     * @name PasteDecrypter
3812
     * @class
3813
     */
3814
    var PasteDecrypter = (function () {
3815
        var me = {};
3816
3817
        /**
3818
         * decrypt data or prompts for password in cvase of failure
3819
         *
3820
         * @name   PasteDecrypter.decryptOrPromptPassword
3821
         * @private
3822
         * @function
3823
         * @param  {string} key
3824
         * @param  {string} password - optional, may be an empty string
3825
         * @param  {string} cipherdata
3826
         * @throws {string}
3827
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3828
         */
3829
        function decryptOrPromptPassword(key, password, cipherdata)
3830
        {
3831
            // try decryption without password
3832
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3833
3834
            // if it fails, request password
3835
            if (plaindata.length === 0 && password.length === 0) {
3836
                // try to get cached password first
3837
                password = Prompt.getPassword();
3838
3839
                // if password is there, re-try
3840
                if (password.length === 0) {
3841
                    password = Prompt.requestPassword();
3842
                }
3843
                // recursive
3844
                // note: an infinite loop is prevented as the previous if
3845
                // clause checks whether a password is already set and ignores
3846
                // errors when a password has been passed
3847
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3848
            }
3849
3850
            // if all tries failed, we can only return an error
3851
            if (plaindata.length === 0) {
3852
                throw 'failed to decipher data';
3853
            }
3854
3855
            return plaindata;
3856
        }
3857
3858
        /**
3859
         * decrypt the actual paste text
3860
         *
3861
         * @name   PasteDecrypter.decryptOrPromptPassword
3862
         * @private
3863
         * @function
3864
         * @param  {object} paste - paste data in object form
3865
         * @param  {string} key
3866
         * @param  {string} password
3867
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3868
         * @return {bool} whether action was successful
3869
         * @throws {string}
3870
         */
3871
        function decryptPaste(paste, key, password, ignoreError)
3872
        {
3873
            var plaintext;
3874
            if (ignoreError === true) {
3875
                plaintext = CryptTool.decipher(key, password, paste.data);
3876
            } else {
3877
                try {
3878
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3879
                } catch (err) {
3880
                    throw 'failed to decipher paste text: ' + err;
3881
                }
3882
                if (plaintext === false) {
3883
                    return false;
3884
                }
3885
            }
3886
3887
            // on success show paste
3888
            PasteViewer.setFormat(paste.meta.formatter);
3889
            PasteViewer.setText(plaintext);
3890
            // trigger to show the text (attachment loaded afterwards)
3891
            PasteViewer.run();
3892
3893
            return true;
3894
        }
3895
3896
        /**
3897
         * decrypts any attachment
3898
         *
3899
         * @name   PasteDecrypter.decryptAttachment
3900
         * @private
3901
         * @function
3902
         * @param  {object} paste - paste data in object form
3903
         * @param  {string} key
3904
         * @param  {string} password
3905
         * @return {bool} whether action was successful
3906
         * @throws {string}
3907
         */
3908
        function decryptAttachment(paste, key, password)
3909
        {
3910
            var attachment, attachmentName;
3911
3912
            // decrypt attachment
3913
            try {
3914
                attachment = decryptOrPromptPassword(key, password, paste.attachment);
3915
            } catch (err) {
3916
                throw 'failed to decipher attachment: ' + err;
3917
            }
3918
            if (attachment === false) {
3919
                return false;
3920
            }
3921
3922
            // decrypt attachment name
3923
            if (paste.attachmentname) {
3924
                try {
3925
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3926
                } catch (err) {
3927
                    throw 'failed to decipher attachment name: ' + err;
3928
                }
3929
                if (attachmentName === false) {
3930
                    return false;
3931
                }
3932
            }
3933
3934
            AttachmentViewer.setAttachment(attachment, attachmentName);
3935
            AttachmentViewer.showAttachment();
3936
3937
            return true;
3938
        }
3939
3940
        /**
3941
         * decrypts all comments and shows them
3942
         *
3943
         * @name   PasteDecrypter.decryptComments
3944
         * @private
3945
         * @function
3946
         * @param  {object} paste - paste data in object form
3947
         * @param  {string} key
3948
         * @param  {string} password
3949
         * @return {bool} whether action was successful
3950
         */
3951
        function decryptComments(paste, key, password)
3952
        {
3953
            // remove potentially previous discussion
3954
            DiscussionViewer.prepareNewDiscussion();
3955
3956
            // iterate over comments
3957
            for (var i = 0; i < paste.comments.length; ++i) {
3958
                var comment = paste.comments[i];
3959
3960
                DiscussionViewer.addComment(
3961
                    comment,
3962
                    CryptTool.decipher(key, password, comment.data),
3963
                    CryptTool.decipher(key, password, comment.meta.nickname)
3964
                );
3965
            }
3966
3967
            DiscussionViewer.finishDiscussion();
3968
            return true;
3969
        }
3970
3971
        /**
3972
         * show decrypted text in the display area, including discussion (if open)
3973
         *
3974
         * @name   PasteDecrypter.run
3975
         * @function
3976
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3977
         */
3978
        me.run = function(paste)
3979
        {
3980
            Alert.hideMessages();
3981
            Alert.showLoading('Decrypting paste…', 'cloud-download');
3982
3983
            if (typeof paste === 'undefined') {
3984
                paste = $.parseJSON(Model.getCipherData());
3985
            }
3986
3987
            var key = Model.getPasteKey(),
3988
                password = Prompt.getPassword();
3989
3990
            if (PasteViewer.isPrettyPrinted()) {
3991
                // don't decrypt twice
3992
                return;
3993
            }
3994
3995
            // try to decrypt the paste
3996
            try {
3997
                // decrypt attachments
3998
                if (paste.attachment) {
3999
                    if (AttachmentViewer.hasAttachmentData()) {
4000
                        // try to decrypt paste and if it fails (because the password is
4001
                        // missing) return to let JS continue and wait for user
4002
                        if (!decryptAttachment(paste, key, password)) {
4003
                            return;
4004
                        }
4005
                    }
4006
                    // ignore empty paste, as this is allowed when pasting attachments
4007
                    decryptPaste(paste, key, password, true);
4008
                } else {
4009
                    decryptPaste(paste, key, password);
4010
                }
4011
4012
4013
                // shows the remaining time (until) deletion
4014
                PasteStatus.showRemainingTime(paste.meta);
4015
4016
                // if the discussion is opened on this paste, display it
4017
                if (paste.meta.opendiscussion) {
4018
                    decryptComments(paste, key, password);
4019
                }
4020
4021
                Alert.hideLoading();
4022
                TopNav.showViewButtons();
4023
            } catch(err) {
4024
                Alert.hideLoading();
4025
4026
                // log and show error
4027
                console.error(err);
4028
                Alert.showError('Could not decrypt data (Wrong key?)');
4029
            }
4030
        };
4031
4032
        /**
4033
         * initialize
4034
         *
4035
         * @name   PasteDecrypter.init
4036
         * @function
4037
         */
4038
        me.init = function()
4039
        {
4040
            // nothing yet
4041
        };
4042
4043
        return me;
4044
    })();
4045
4046
    /**
4047
     * (controller) main PrivateBin logic
4048
     *
4049
     * @name   Controller
4050
     * @param  {object} window
4051
     * @param  {object} document
4052
     * @class
4053
     */
4054
    var Controller = (function (window, document) {
4055
        var me = {};
4056
4057
        /**
4058
         * hides all status messages no matter which module showed them
4059
         *
4060
         * @name   Controller.hideStatusMessages
4061
         * @function
4062
         */
4063
        me.hideStatusMessages = function()
4064
        {
4065
            PasteStatus.hideMessages();
4066
            Alert.hideMessages();
4067
        };
4068
4069
        /**
4070
         * creates a new paste
4071
         *
4072
         * @name   Controller.newPaste
4073
         * @function
4074
         */
4075
        me.newPaste = function()
4076
        {
4077
            // Important: This *must not* run Alert.hideMessages() as previous
4078
            // errors from viewing a paste should be shown.
4079
            TopNav.hideAllButtons();
4080
            Alert.showLoading('Preparing new paste…', 'time');
4081
4082
            PasteStatus.hideMessages();
4083
            PasteViewer.hide();
4084
            Editor.resetInput();
4085
            Editor.show();
4086
            Editor.focusInput();
4087
            AttachmentViewer.removeAttachment();
4088
4089
            TopNav.showCreateButtons();
4090
            Alert.hideLoading();
4091
        };
4092
4093
        /**
4094
         * shows the loaded paste
4095
         *
4096
         * @name   Controller.showPaste
4097
         * @function
4098
         */
4099
        me.showPaste = function()
4100
        {
4101
            try {
4102
                Model.getPasteId();
4103
                Model.getPasteKey();
4104
            } catch (err) {
4105
                console.error(err);
4106
4107
                // missing decryption key (or paste ID) in URL?
4108
                if (window.location.hash.length === 0) {
4109
                    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?)');
4110
                    return;
4111
                }
4112
            }
4113
4114
            // show proper elements on screen
4115
            PasteDecrypter.run();
4116
        };
4117
4118
        /**
4119
         * refreshes the loaded paste to show potential new data
4120
         *
4121
         * @name   Controller.refreshPaste
4122
         * @function
4123
         * @param  {function} callback
4124
         */
4125
        me.refreshPaste = function(callback)
4126
        {
4127
            // save window position to restore it later
4128
            var orgPosition = $(window).scrollTop();
4129
4130
            Uploader.prepare();
4131
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
4132
4133
            Uploader.setFailure(function (status, data) {
4134
                // revert loading status…
4135
                Alert.hideLoading();
4136
                TopNav.showViewButtons();
4137
4138
                // show error message
4139
                Alert.showError(
4140
                    Uploader.parseUploadError(status, data, 'refresh display')
4141
                );
4142
            });
4143
            Uploader.setSuccess(function (status, data) {
4144
                PasteDecrypter.run(data);
4145
4146
                // restore position
4147
                window.scrollTo(0, orgPosition);
4148
4149
                callback();
4150
            });
4151
            Uploader.run();
4152
        };
4153
4154
        /**
4155
         * clone the current paste
4156
         *
4157
         * @name   Controller.clonePaste
4158
         * @function
4159
         */
4160
        me.clonePaste = function()
4161
        {
4162
            TopNav.collapseBar();
4163
            TopNav.hideAllButtons();
4164
            Alert.showLoading('Cloning paste…', 'transfer');
4165
4166
            // hide messages from previous paste
4167
            me.hideStatusMessages();
4168
4169
            // erase the id and the key in url
4170
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
4171
4172
            if (AttachmentViewer.hasAttachment()) {
4173
                AttachmentViewer.moveAttachmentTo(
4174
                    TopNav.getCustomAttachment(),
4175
                    'Cloned: \'%s\''
4176
                );
4177
                TopNav.hideFileSelector();
4178
                AttachmentViewer.hideAttachment();
4179
                // NOTE: it also looks nice without removing the attachment
4180
                // but for a consistent display we remove it…
4181
                AttachmentViewer.hideAttachmentPreview();
4182
                TopNav.showCustomAttachment();
4183
4184
                // show another status message to make the user aware that the
4185
                // file was cloned too!
4186
                Alert.showStatus(
4187
                    [
4188
                        'The cloned file \'%s\' was attached to this paste.',
4189
                        AttachmentViewer.getAttachment()[1]
4190
                    ],
4191
                    'copy'
4192
                );
4193
            }
4194
4195
            Editor.setText(PasteViewer.getText());
4196
            PasteViewer.hide();
4197
            Editor.show();
4198
4199
            Alert.hideLoading();
4200
            TopNav.showCreateButtons();
4201
        };
4202
4203
        /**
4204
         * removes a saved paste
4205
         *
4206
         * @name   Controller.removePaste
4207
         * @function
4208
         * @param  {string} pasteId
4209
         * @param  {string} deleteToken
4210
         */
4211
        me.removePaste = function(pasteId, deleteToken) {
4212
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
4213
            // so we use a POST request
4214
            Uploader.prepare();
4215
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
4216
            Uploader.setUnencryptedData('deletetoken', deleteToken);
4217
4218
            Uploader.setFailure(function () {
4219
                Alert.showError(
4220
                    I18n._('Could not delete the paste, it was not stored in burn after reading mode.')
4221
                );
4222
            });
4223
            Uploader.run();
4224
        };
4225
4226
        /**
4227
         * application start
4228
         *
4229
         * @name   Controller.init
4230
         * @function
4231
         */
4232
        me.init = function()
4233
        {
4234
            // first load translations
4235
            I18n.loadTranslations();
4236
4237
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
4238
4239
            // initialize other modules/"classes"
4240
            Alert.init();
4241
            Model.init();
4242
            AttachmentViewer.init();
4243
            DiscussionViewer.init();
4244
            Editor.init();
4245
            PasteDecrypter.init();
4246
            PasteEncrypter.init();
4247
            PasteStatus.init();
4248
            PasteViewer.init();
4249
            Prompt.init();
4250
            TopNav.init();
4251
            UiHelper.init();
4252
            Uploader.init();
4253
4254
            // display an existing paste
4255
            if (Model.hasCipherData()) {
4256
                return me.showPaste();
4257
            }
4258
4259
            // otherwise create a new paste
4260
            me.newPaste();
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...
4261
        };
4262
4263
        return me;
4264
    })(window, document);
4265
4266
    return {
4267
        Helper: Helper,
4268
        I18n: I18n,
4269
        CryptTool: CryptTool,
4270
        Model: Model,
4271
        UiHelper: UiHelper,
4272
        Alert: Alert,
4273
        PasteStatus: PasteStatus,
4274
        Prompt: Prompt,
4275
        Editor: Editor,
4276
        PasteViewer: PasteViewer,
4277
        AttachmentViewer: AttachmentViewer,
4278
        DiscussionViewer: DiscussionViewer,
4279
        TopNav: TopNav,
4280
        Uploader: Uploader,
4281
        PasteEncrypter: PasteEncrypter,
4282
        PasteDecrypter: PasteDecrypter,
4283
        Controller: Controller
4284
    };
4285
})(jQuery, sjcl, Base64, RawDeflate);
4286