Passed
Push — master ( da45d3...429d43 )
by rugk
03:45
created

js/privatebin.js (8 issues)

Code
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
0 ignored issues
show
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
151
         * @param  {...*} args - one or multiple parameters injected into format string
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
152
         * @return {string}
153
         */
154
        me.sprintf = function()
155
        {
156
            var args = Array.prototype.slice.call(arguments);
157
            var format = args[0],
158
                i = 1;
159
            return format.replace(/%(s|d)/g, function (m) {
160
                // m is the matched format, e.g. %s, %d
161
                var val = args[i];
162
                // A switch statement so that the formatter can be extended.
163
                switch (m)
164
                {
165
                    case '%d':
166
                        val = parseFloat(val);
167
                        if (isNaN(val)) {
168
                            val = 0;
169
                        }
170
                        break;
171
                    default:
172
                        // Default is %s
173
                }
174
                ++i;
175
                return val;
176
            });
177
        };
178
179
        /**
180
         * get value of cookie, if it was set, empty string otherwise
181
         *
182
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
183
         * @name   Helper.getCookie
184
         * @function
185
         * @param  {string} cname - may not be empty
186
         * @return {string}
187
         */
188
        me.getCookie = function(cname) {
189
            var name = cname + '=',
190
                ca = document.cookie.split(';');
191
            for (var i = 0; i < ca.length; ++i) {
192
                var c = ca[i];
193
                while (c.charAt(0) === ' ')
194
                {
195
                    c = c.substring(1);
196
                }
197
                if (c.indexOf(name) === 0)
198
                {
199
                    return c.substring(name.length, c.length);
200
                }
201
            }
202
            return '';
203
        };
204
205
        /**
206
         * get the current location (without search or hash part of the URL),
207
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
208
         *
209
         * @name   Helper.baseUri
210
         * @function
211
         * @return {string}
212
         */
213
        me.baseUri = function()
214
        {
215
            // check for cached version
216
            if (baseUri !== null) {
217
                return baseUri;
218
            }
219
220
            baseUri = window.location.origin + window.location.pathname;
221
            return baseUri;
222
        };
223
224
        /**
225
         * resets state, used for unit testing
226
         *
227
         * @name   Helper.reset
228
         * @function
229
         */
230
        me.reset = function()
231
        {
232
            baseUri = null;
233
        };
234
235
        return me;
236
    })();
237
238
    /**
239
     * internationalization module
240
     *
241
     * @name I18n
242
     * @class
243
     */
244
    var I18n = (function () {
245
        var me = {};
246
247
        /**
248
         * const for string of loaded language
249
         *
250
         * @name I18n.languageLoadedEvent
251
         * @private
252
         * @prop   {string}
253
         * @readonly
254
         */
255
        var languageLoadedEvent = 'languageLoaded';
256
257
        /**
258
         * supported languages, minus the built in 'en'
259
         *
260
         * @name I18n.supportedLanguages
261
         * @private
262
         * @prop   {string[]}
263
         * @readonly
264
         */
265
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
266
267
        /**
268
         * built in language
269
         *
270
         * @name I18n.language
271
         * @private
272
         * @prop   {string|null}
273
         */
274
        var language = null;
275
276
        /**
277
         * translation cache
278
         *
279
         * @name I18n.translations
280
         * @private
281
         * @enum   {Object}
282
         */
283
        var translations = {};
284
285
        /**
286
         * translate a string, alias for I18n.translate
287
         *
288
         * @name   I18n._
289
         * @function
290
         * @param  {jQuery} $element - optional
291
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
292
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
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
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
314
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
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}));
0 ignored issues
show
The variable PopStateEvent seems to be never declared. If this is a global, consider adding a /** global: PopStateEvent */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
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
        readFileData = function (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
        addDragDropHandler = function () {
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
        addClipboardEventHandler = function () {
2292
            $(document).on('paste',
2293
                    function (event) {
2294
                        var items = (event.clipboardData || event.originalEvent.clipboardData).items;
2295
                        for (var i in items) {
2296
                            if (items.hasOwnProperty(i)) {
2297
                                var item = items[i];
2298
                                if (item.kind === 'file') {
2299
                                    readFileData(item.getAsFile());
2300
                                }
2301
                            }
2302
                        }
2303
                    });
2304
        };
2305
2306
2307
        /**
2308
         * getter for attachment data
2309
         *
2310
         * @name   AttachmentViewer.getAttachmentData
2311
         * @function
2312
         * @return {jQuery}
2313
         */
2314
        me.getAttachmentData = function () {
2315
            return attachmentData;
2316
        };
2317
2318
        /**
2319
         * getter for attachment link
2320
         *
2321
         * @name   AttachmentViewer.getAttachmentLink
2322
         * @function
2323
         * @return {jQuery}
2324
         */
2325
        me.getAttachmentLink = function () {
2326
            return $attachmentLink;
2327
        };
2328
2329
        /**
2330
         * getter for attachment preview
2331
         *
2332
         * @name   AttachmentViewer.getAttachmentPreview
2333
         * @function
2334
         * @return {jQuery}
2335
         */
2336
        me.getAttachmentPreview = function () {
2337
            return $attachmentPreview;
2338
        };
2339
2340
        /**
2341
         * getter for file data, returns the file contents
2342
         *
2343
         * @name   AttachmentViewer.getFile
2344
         * @function
2345
         * @return {string}
2346
         */
2347
        me.getFile = function () {
2348
            return file;
2349
        };
2350
2351
        /**
2352
         * initiate
2353
         *
2354
         * preloads jQuery elements
2355
         *
2356
         * @name   AttachmentViewer.init
2357
         * @function
2358
         */
2359
        me.init = function()
2360
        {
2361
            $attachment = $('#attachment');
2362
            if($attachment.length){
2363
                $attachmentLink = $('#attachment a');
2364
                $attachmentPreview = $('#attachmentPreview');
2365
                $dragAndDropFileName = $('#dragAndDropFileName');
2366
2367
                $fileInput = $('#file');
2368
                addDragDropHandler();
2369
                addClipboardEventHandler();
2370
            }
2371
        }
2372
2373
        return me;
2374
    })();
2375
2376
    /**
2377
     * (view) Shows discussion thread and handles replies
2378
     *
2379
     * @name   DiscussionViewer
2380
     * @class
2381
     */
2382
    var DiscussionViewer = (function () {
2383
        var me = {};
2384
2385
        var $commentTail,
2386
            $discussion,
2387
            $reply,
2388
            $replyMessage,
2389
            $replyNickname,
2390
            $replyStatus,
2391
            $commentContainer;
2392
2393
        var replyCommentId;
2394
2395
        /**
2396
         * initializes the templates
2397
         *
2398
         * @name   DiscussionViewer.initTemplates
2399
         * @private
2400
         * @function
2401
         */
2402
        function initTemplates()
2403
        {
2404
            $reply = Model.getTemplate('reply');
2405
            $replyMessage = $reply.find('#replymessage');
2406
            $replyNickname = $reply.find('#nickname');
2407
            $replyStatus = $reply.find('#replystatus');
2408
2409
            // cache jQuery elements
2410
            $commentTail = Model.getTemplate('commenttail');
2411
        }
2412
2413
        /**
2414
         * open the comment entry when clicking the "Reply" button of a comment
2415
         *
2416
         * @name   DiscussionViewer.openReply
2417
         * @private
2418
         * @function
2419
         * @param  {Event} event
2420
         */
2421
        function openReply(event)
2422
        {
2423
            var $source = $(event.target);
2424
2425
            // clear input
2426
            $replyMessage.val('');
2427
            $replyNickname.val('');
2428
2429
            // get comment id from source element
2430
            replyCommentId = $source.parent().prop('id').split('_')[1];
2431
2432
            // move to correct position
2433
            $source.after($reply);
2434
2435
            // show
2436
            $reply.removeClass('hidden');
2437
            $replyMessage.focus();
2438
2439
            event.preventDefault();
2440
        }
2441
2442
        /**
2443
         * custom handler for displaying notifications in own status message area
2444
         *
2445
         * @name   DiscussionViewer.handleNotification
2446
         * @function
2447
         * @param  {string} alertType
2448
         * @return {bool|jQuery}
2449
         */
2450
        me.handleNotification = function(alertType)
2451
        {
2452
            // ignore loading messages
2453
            if (alertType === 'loading') {
2454
                return false;
2455
            }
2456
2457
            if (alertType === 'danger') {
2458
                $replyStatus.removeClass('alert-info');
2459
                $replyStatus.addClass('alert-danger');
2460
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2461
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2462
            } else {
2463
                $replyStatus.removeClass('alert-danger');
2464
                $replyStatus.addClass('alert-info');
2465
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2466
                $replyStatus.find(':first').addClass('glyphicon-alert');
2467
            }
2468
2469
            return $replyStatus;
2470
        };
2471
2472
        /**
2473
         * adds another comment
2474
         *
2475
         * @name   DiscussionViewer.addComment
2476
         * @function
2477
         * @param {object} comment
2478
         * @param {string} commentText
2479
         * @param {string} nickname
2480
         */
2481
        me.addComment = function(comment, commentText, nickname)
2482
        {
2483
            if (commentText === '') {
2484
                commentText = 'comment decryption failed';
2485
            }
2486
2487
            // create new comment based on template
2488
            var $commentEntry = Model.getTemplate('comment');
2489
            $commentEntry.prop('id', 'comment_' + comment.id);
2490
            var $commentEntryData = $commentEntry.find('div.commentdata');
2491
2492
            // set & parse text
2493
            $commentEntryData.html(
2494
                DOMPurify.sanitize(
2495
                    Helper.urls2links(commentText)
2496
                )
2497
            );
2498
2499
            // set nickname
2500
            if (nickname.length > 0) {
2501
                $commentEntry.find('span.nickname').text(nickname);
2502
            } else {
2503
                $commentEntry.find('span.nickname').html('<i></i>');
2504
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2505
            }
2506
2507
            // set date
2508
            $commentEntry.find('span.commentdate')
2509
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2510
                      .attr('title', 'CommentID: ' + comment.id);
2511
2512
            // if an avatar is available, display it
2513
            if (comment.meta.vizhash) {
2514
                $commentEntry.find('span.nickname')
2515
                             .before(
2516
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2517
                             );
2518
                $(document).on('languageLoaded', function () {
2519
                    $commentEntry.find('img.vizhash')
2520
                                 .prop('title', I18n._('Avatar generated from IP address'));
2521
                });
2522
            }
2523
2524
            // starting point (default value/fallback)
2525
            var $place = $commentContainer;
2526
2527
            // if parent comment exists
2528
            var $parentComment = $('#comment_' + comment.parentid);
2529
            if ($parentComment.length) {
2530
                // use parent as position for new comment, so it is shifted
2531
                // to the right
2532
                $place = $parentComment;
2533
            }
2534
2535
            // finally append comment
2536
            $place.append($commentEntry);
2537
        };
2538
2539
        /**
2540
         * finishes the discussion area after last comment
2541
         *
2542
         * @name   DiscussionViewer.finishDiscussion
2543
         * @function
2544
         */
2545
        me.finishDiscussion = function()
2546
        {
2547
            // add 'add new comment' area
2548
            $commentContainer.append($commentTail);
2549
2550
            // show discussions
2551
            $discussion.removeClass('hidden');
2552
        };
2553
2554
        /**
2555
         * removes the old discussion and prepares everything for creating a new
2556
         * one.
2557
         *
2558
         * @name   DiscussionViewer.prepareNewDiscussion
2559
         * @function
2560
         */
2561
        me.prepareNewDiscussion = function()
2562
        {
2563
            $commentContainer.html('');
2564
            $discussion.addClass('hidden');
2565
2566
            // (re-)init templates
2567
            initTemplates();
2568
        };
2569
2570
        /**
2571
         * returns the users message from the reply form
2572
         *
2573
         * @name   DiscussionViewer.getReplyMessage
2574
         * @function
2575
         * @return {String}
2576
         */
2577
        me.getReplyMessage = function()
2578
        {
2579
            return $replyMessage.val();
2580
        };
2581
2582
        /**
2583
         * returns the users nickname (if any) from the reply form
2584
         *
2585
         * @name   DiscussionViewer.getReplyNickname
2586
         * @function
2587
         * @return {String}
2588
         */
2589
        me.getReplyNickname = function()
2590
        {
2591
            return $replyNickname.val();
2592
        };
2593
2594
        /**
2595
         * returns the id of the parent comment the user is replying to
2596
         *
2597
         * @name   DiscussionViewer.getReplyCommentId
2598
         * @function
2599
         * @return {int|undefined}
2600
         */
2601
        me.getReplyCommentId = function()
2602
        {
2603
            return replyCommentId;
2604
        };
2605
2606
        /**
2607
         * highlights a specific comment and scrolls to it if necessary
2608
         *
2609
         * @name   DiscussionViewer.highlightComment
2610
         * @function
2611
         * @param {string} commentId
2612
         * @param {bool} fadeOut - whether to fade out the comment
2613
         */
2614
        me.highlightComment = function(commentId, fadeOut)
2615
        {
2616
            var $comment = $('#comment_' + commentId);
2617
            // in case comment does not exist, cancel
2618
            if ($comment.length === 0) {
2619
                return;
2620
            }
2621
2622
            var highlightComment = function () {
2623
                $comment.addClass('highlight');
2624
                if (fadeOut === true) {
2625
                    setTimeout(function () {
2626
                        $comment.removeClass('highlight');
2627
                    }, 300);
2628
                }
2629
            };
2630
2631
            if (UiHelper.isVisible($comment)) {
2632
                return highlightComment();
2633
            }
2634
2635
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2636
        };
2637
2638
        /**
2639
         * initiate
2640
         *
2641
         * preloads jQuery elements
2642
         *
2643
         * @name   DiscussionViewer.init
2644
         * @function
2645
         */
2646
        me.init = function()
2647
        {
2648
            // bind events to templates (so they are later cloned)
2649
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2650
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2651
2652
            $commentContainer = $('#commentcontainer');
2653
            $discussion = $('#discussion');
2654
        };
2655
2656
        return me;
2657
    })();
2658
2659
    /**
2660
     * Manage top (navigation) bar
2661
     *
2662
     * @name   TopNav
2663
     * @param  {object} window
2664
     * @param  {object} document
2665
     * @class
2666
     */
2667
    var TopNav = (function (window, document) {
2668
        var me = {};
2669
2670
        var createButtonsDisplayed = false;
2671
        var viewButtonsDisplayed = false;
2672
2673
        var $attach,
2674
            $burnAfterReading,
2675
            $burnAfterReadingOption,
2676
            $cloneButton,
2677
            $customAttachment,
2678
            $expiration,
2679
            $fileRemoveButton,
2680
            $fileWrap,
2681
            $formatter,
2682
            $newButton,
2683
            $openDiscussion,
2684
            $openDiscussionOption,
2685
            $password,
2686
            $passwordInput,
2687
            $rawTextButton,
2688
            $qrCodeLink,
2689
            $sendButton;
2690
2691
        var pasteExpiration = '1week';
2692
2693
        /**
2694
         * set the expiration on bootstrap templates in dropdown
2695
         *
2696
         * @name   TopNav.updateExpiration
2697
         * @private
2698
         * @function
2699
         * @param  {Event} event
2700
         */
2701
        function updateExpiration(event)
2702
        {
2703
            // get selected option
2704
            var target = $(event.target);
2705
2706
            // update dropdown display and save new expiration time
2707
            $('#pasteExpirationDisplay').text(target.text());
2708
            pasteExpiration = target.data('expiration');
2709
2710
            event.preventDefault();
2711
        }
2712
2713
        /**
2714
         * set the format on bootstrap templates in dropdown
2715
         *
2716
         * @name   TopNav.updateFormat
2717
         * @private
2718
         * @function
2719
         * @param  {Event} event
2720
         */
2721
        function updateFormat(event)
2722
        {
2723
            // get selected option
2724
            var $target = $(event.target);
2725
2726
            // update dropdown display and save new format
2727
            var newFormat = $target.data('format');
2728
            $('#pasteFormatterDisplay').text($target.text());
2729
            PasteViewer.setFormat(newFormat);
2730
2731
            // update preview
2732
            if (Editor.isPreview()) {
2733
                PasteViewer.run();
2734
            }
2735
2736
            event.preventDefault();
2737
        }
2738
2739
        /**
2740
         * when "burn after reading" is checked, disable discussion
2741
         *
2742
         * @name   TopNav.changeBurnAfterReading
2743
         * @private
2744
         * @function
2745
         */
2746
        function changeBurnAfterReading()
2747
        {
2748
            if ($burnAfterReading.is(':checked')) {
2749
                $openDiscussionOption.addClass('buttondisabled');
2750
                $openDiscussion.prop('checked', false);
2751
2752
                // if button is actually disabled, force-enable it and uncheck other button
2753
                $burnAfterReadingOption.removeClass('buttondisabled');
2754
            } else {
2755
                $openDiscussionOption.removeClass('buttondisabled');
2756
            }
2757
        }
2758
2759
        /**
2760
         * when discussion is checked, disable "burn after reading"
2761
         *
2762
         * @name   TopNav.changeOpenDiscussion
2763
         * @private
2764
         * @function
2765
         */
2766
        function changeOpenDiscussion()
2767
        {
2768
            if ($openDiscussion.is(':checked')) {
2769
                $burnAfterReadingOption.addClass('buttondisabled');
2770
                $burnAfterReading.prop('checked', false);
2771
2772
                // if button is actually disabled, force-enable it and uncheck other button
2773
                $openDiscussionOption.removeClass('buttondisabled');
2774
            } else {
2775
                $burnAfterReadingOption.removeClass('buttondisabled');
2776
            }
2777
        }
2778
2779
        /**
2780
         * return raw text
2781
         *
2782
         * @name   TopNav.rawText
2783
         * @private
2784
         * @function
2785
         */
2786
        function rawText()
2787
        {
2788
            TopNav.hideAllButtons();
2789
            Alert.showLoading('Showing raw text…', 'time');
2790
            var paste = PasteViewer.getText();
2791
2792
            // push a new state to allow back navigation with browser back button
2793
            history.pushState(
2794
                {type: 'raw'},
2795
                document.title,
2796
                // recreate paste URL
2797
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2798
                Model.getPasteKey()
2799
            );
2800
2801
            // we use text/html instead of text/plain to avoid a bug when
2802
            // reloading the raw text view (it reverts to type text/html)
2803
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2804
            var newDoc = document.open('text/html', 'replace');
2805
            newDoc.write('<!DOCTYPE html><html><head>');
2806
            for (var i = 0; i < $head.length; i++) {
2807
                newDoc.write($head[i].outerHTML);
2808
            }
2809
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2810
            newDoc.close();
2811
        }
2812
2813
        /**
2814
         * saves the language in a cookie and reloads the page
2815
         *
2816
         * @name   TopNav.setLanguage
2817
         * @private
2818
         * @function
2819
         * @param  {Event} event
2820
         */
2821
        function setLanguage(event)
2822
        {
2823
            document.cookie = 'lang=' + $(event.target).data('lang');
2824
            UiHelper.reloadHome();
2825
        }
2826
2827
        /**
2828
         * hides all messages and creates a new paste
2829
         *
2830
         * @name   TopNav.clickNewPaste
2831
         * @private
2832
         * @function
2833
         */
2834
        function clickNewPaste()
2835
        {
2836
            Controller.hideStatusMessages();
2837
            Controller.newPaste();
2838
        }
2839
2840
        /**
2841
         * removes the existing attachment
2842
         *
2843
         * @name   TopNav.removeAttachment
2844
         * @private
2845
         * @function
2846
         * @param  {Event} event
2847
         */
2848
        function removeAttachment(event)
2849
        {
2850
            // if custom attachment is used, remove it first
2851
            if (!$customAttachment.hasClass('hidden')) {
2852
                AttachmentViewer.removeAttachment();
2853
                $customAttachment.addClass('hidden');
2854
                $fileWrap.removeClass('hidden');
2855
            }
2856
2857
            // in any case, remove saved attachment data
2858
            AttachmentViewer.removeAttachmentData();
2859
2860
            // hide UI for selected files
2861
            // our up-to-date jQuery can handle it :)
2862
            $fileWrap.find('input').val('');
2863
            AttachmentViewer.clearDragAndDrop();
2864
2865
            // pevent '#' from appearing in the URL
2866
            event.preventDefault();
2867
        }
2868
2869
        /**
2870
         * Shows the QR code of the current paste (URL).
2871
         *
2872
         * @name   TopNav.displayQrCode
2873
         * @private
2874
         * @function
2875
         */
2876
        function displayQrCode()
2877
        {
2878
            var qrCanvas = kjua({
2879
                render: 'canvas',
2880
                text: window.location.href
2881
            });
2882
            $('#qrcode-display').html(qrCanvas);
2883
        }
2884
2885
        /**
2886
         * Shows all navigation elements for viewing an existing paste
2887
         *
2888
         * @name   TopNav.showViewButtons
2889
         * @function
2890
         */
2891
        me.showViewButtons = function()
2892
        {
2893
            if (viewButtonsDisplayed) {
2894
                console.warn('showViewButtons: view buttons are already displayed');
2895
                return;
2896
            }
2897
2898
            $newButton.removeClass('hidden');
2899
            $cloneButton.removeClass('hidden');
2900
            $rawTextButton.removeClass('hidden');
2901
            $qrCodeLink.removeClass('hidden');
2902
2903
            viewButtonsDisplayed = true;
2904
        };
2905
2906
        /**
2907
         * Hides all navigation elements for viewing an existing paste
2908
         *
2909
         * @name   TopNav.hideViewButtons
2910
         * @function
2911
         */
2912
        me.hideViewButtons = function()
2913
        {
2914
            if (!viewButtonsDisplayed) {
2915
                console.warn('hideViewButtons: view buttons are already hidden');
2916
                return;
2917
            }
2918
2919
            $newButton.addClass('hidden');
2920
            $cloneButton.addClass('hidden');
2921
            $rawTextButton.addClass('hidden');
2922
            $qrCodeLink.addClass('hidden');
2923
2924
            viewButtonsDisplayed = false;
2925
        };
2926
2927
        /**
2928
         * Hides all elements belonging to existing pastes
2929
         *
2930
         * @name   TopNav.hideAllButtons
2931
         * @function
2932
         */
2933
        me.hideAllButtons = function()
2934
        {
2935
            me.hideViewButtons();
2936
            me.hideCreateButtons();
2937
        };
2938
2939
        /**
2940
         * shows all elements needed when creating a new paste
2941
         *
2942
         * @name   TopNav.showCreateButtons
2943
         * @function
2944
         */
2945
        me.showCreateButtons = function()
2946
        {
2947
            if (createButtonsDisplayed) {
2948
                console.warn('showCreateButtons: create buttons are already displayed');
2949
                return;
2950
            }
2951
2952
            $sendButton.removeClass('hidden');
2953
            $expiration.removeClass('hidden');
2954
            $formatter.removeClass('hidden');
2955
            $burnAfterReadingOption.removeClass('hidden');
2956
            $openDiscussionOption.removeClass('hidden');
2957
            $newButton.removeClass('hidden');
2958
            $password.removeClass('hidden');
2959
            $attach.removeClass('hidden');
2960
2961
            createButtonsDisplayed = true;
2962
        };
2963
2964
        /**
2965
         * shows all elements needed when creating a new paste
2966
         *
2967
         * @name   TopNav.hideCreateButtons
2968
         * @function
2969
         */
2970
        me.hideCreateButtons = function()
2971
        {
2972
            if (!createButtonsDisplayed) {
2973
                console.warn('hideCreateButtons: create buttons are already hidden');
2974
                return;
2975
            }
2976
2977
            $newButton.addClass('hidden');
2978
            $sendButton.addClass('hidden');
2979
            $expiration.addClass('hidden');
2980
            $formatter.addClass('hidden');
2981
            $burnAfterReadingOption.addClass('hidden');
2982
            $openDiscussionOption.addClass('hidden');
2983
            $password.addClass('hidden');
2984
            $attach.addClass('hidden');
2985
2986
            createButtonsDisplayed = false;
2987
        };
2988
2989
        /**
2990
         * only shows the "new paste" button
2991
         *
2992
         * @name   TopNav.showNewPasteButton
2993
         * @function
2994
         */
2995
        me.showNewPasteButton = function()
2996
        {
2997
            $newButton.removeClass('hidden');
2998
        };
2999
3000
        /**
3001
         * only hides the clone button
3002
         *
3003
         * @name   TopNav.hideCloneButton
3004
         * @function
3005
         */
3006
        me.hideCloneButton = function()
3007
        {
3008
            $cloneButton.addClass('hidden');
3009
        };
3010
3011
        /**
3012
         * only hides the raw text button
3013
         *
3014
         * @name   TopNav.hideRawButton
3015
         * @function
3016
         */
3017
        me.hideRawButton = function()
3018
        {
3019
            $rawTextButton.addClass('hidden');
3020
        };
3021
3022
        /**
3023
         * hides the file selector in attachment
3024
         *
3025
         * @name   TopNav.hideFileSelector
3026
         * @function
3027
         */
3028
        me.hideFileSelector = function()
3029
        {
3030
            $fileWrap.addClass('hidden');
3031
        };
3032
3033
3034
        /**
3035
         * shows the custom attachment
3036
         *
3037
         * @name   TopNav.showCustomAttachment
3038
         * @function
3039
         */
3040
        me.showCustomAttachment = function()
3041
        {
3042
            $customAttachment.removeClass('hidden');
3043
        };
3044
3045
        /**
3046
         * collapses the navigation bar, only if expanded
3047
         *
3048
         * @name   TopNav.collapseBar
3049
         * @function
3050
         */
3051
        me.collapseBar = function()
3052
        {
3053
            if ($('#navbar').attr('aria-expanded') === 'true') {
3054
                $('.navbar-toggle').click();
3055
            }
3056
        };
3057
3058
        /**
3059
         * returns the currently set expiration time
3060
         *
3061
         * @name   TopNav.getExpiration
3062
         * @function
3063
         * @return {int}
3064
         */
3065
        me.getExpiration = function()
3066
        {
3067
            return pasteExpiration;
3068
        };
3069
3070
        /**
3071
         * returns the currently selected file(s)
3072
         *
3073
         * @name   TopNav.getFileList
3074
         * @function
3075
         * @return {FileList|null}
3076
         */
3077
        me.getFileList = function()
3078
        {
3079
            var $file = $('#file');
3080
3081
            // if no file given, return null
3082
            if (!$file.length || !$file[0].files.length) {
3083
                return null;
3084
            }
3085
3086
            // ensure the selected file is still accessible
3087
            if (!($file[0].files && $file[0].files[0])) {
3088
                return null;
3089
            }
3090
3091
            return $file[0].files;
3092
        };
3093
3094
        /**
3095
         * returns the state of the burn after reading checkbox
3096
         *
3097
         * @name   TopNav.getExpiration
3098
         * @function
3099
         * @return {bool}
3100
         */
3101
        me.getBurnAfterReading = function()
3102
        {
3103
            return $burnAfterReading.is(':checked');
3104
        };
3105
3106
        /**
3107
         * returns the state of the discussion checkbox
3108
         *
3109
         * @name   TopNav.getOpenDiscussion
3110
         * @function
3111
         * @return {bool}
3112
         */
3113
        me.getOpenDiscussion = function()
3114
        {
3115
            return $openDiscussion.is(':checked');
3116
        };
3117
3118
        /**
3119
         * returns the entered password
3120
         *
3121
         * @name   TopNav.getPassword
3122
         * @function
3123
         * @return {string}
3124
         */
3125
        me.getPassword = function()
3126
        {
3127
            return $passwordInput.val();
3128
        };
3129
3130
        /**
3131
         * returns the element where custom attachments can be placed
3132
         *
3133
         * Used by AttachmentViewer when an attachment is cloned here.
3134
         *
3135
         * @name   TopNav.getCustomAttachment
3136
         * @function
3137
         * @return {jQuery}
3138
         */
3139
        me.getCustomAttachment = function()
3140
        {
3141
            return $customAttachment;
3142
        };
3143
3144
        /**
3145
         * init navigation manager
3146
         *
3147
         * preloads jQuery elements
3148
         *
3149
         * @name   TopNav.init
3150
         * @function
3151
         */
3152
        me.init = function()
3153
        {
3154
            $attach = $('#attach');
3155
            $burnAfterReading = $('#burnafterreading');
3156
            $burnAfterReadingOption = $('#burnafterreadingoption');
3157
            $cloneButton = $('#clonebutton');
3158
            $customAttachment = $('#customattachment');
3159
            $expiration = $('#expiration');
3160
            $fileRemoveButton = $('#fileremovebutton');
3161
            $fileWrap = $('#filewrap');
3162
            $formatter = $('#formatter');
3163
            $newButton = $('#newbutton');
3164
            $openDiscussion = $('#opendiscussion');
3165
            $openDiscussionOption = $('#opendiscussionoption');
3166
            $password = $('#password');
3167
            $passwordInput = $('#passwordinput');
3168
            $rawTextButton = $('#rawtextbutton');
3169
            $sendButton = $('#sendbutton');
3170
            $qrCodeLink = $('#qrcodelink');
3171
3172
            // bootstrap template drop down
3173
            $('#language ul.dropdown-menu li a').click(setLanguage);
3174
            // page template drop down
3175
            $('#language select option').click(setLanguage);
3176
3177
            // bind events
3178
            $burnAfterReading.change(changeBurnAfterReading);
3179
            $openDiscussionOption.change(changeOpenDiscussion);
3180
            $newButton.click(clickNewPaste);
3181
            $sendButton.click(PasteEncrypter.sendPaste);
3182
            $cloneButton.click(Controller.clonePaste);
3183
            $rawTextButton.click(rawText);
3184
            $fileRemoveButton.click(removeAttachment);
3185
            $qrCodeLink.click(displayQrCode);
3186
3187
            // bootstrap template drop downs
3188
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
3189
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
3190
3191
            // initiate default state of checkboxes
3192
            changeBurnAfterReading();
3193
            changeOpenDiscussion();
3194
3195
            // get default value from template or fall back to set value
3196
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
3197
3198
            createButtonsDisplayed = false;
3199
            viewButtonsDisplayed = false;
3200
        };
3201
3202
        return me;
3203
    })(window, document);
3204
3205
    /**
3206
     * Responsible for AJAX requests, transparently handles encryption…
3207
     *
3208
     * @name   Uploader
3209
     * @class
3210
     */
3211
    var Uploader = (function () {
3212
        var me = {};
3213
3214
        var successFunc = null,
3215
            failureFunc = null,
3216
            url,
3217
            data,
3218
            symmetricKey,
3219
            password;
3220
3221
        /**
3222
         * public variable ('constant') for errors to prevent magic numbers
3223
         *
3224
         * @name   Uploader.error
3225
         * @readonly
3226
         * @enum   {Object}
3227
         */
3228
        me.error = {
3229
            okay: 0,
3230
            custom: 1,
3231
            unknown: 2,
3232
            serverError: 3
3233
        };
3234
3235
        /**
3236
         * ajaxHeaders to send in AJAX requests
3237
         *
3238
         * @name   Uploader.ajaxHeaders
3239
         * @private
3240
         * @readonly
3241
         * @enum   {Object}
3242
         */
3243
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
3244
3245
        /**
3246
         * called after successful upload
3247
         *
3248
         * @name   Uploader.checkCryptParameters
3249
         * @private
3250
         * @function
3251
         * @throws {string}
3252
         */
3253
        function checkCryptParameters()
3254
        {
3255
            // workaround for this nasty 'bug' in ECMAScript
3256
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3257
            var typeOfKey = typeof symmetricKey;
3258
            if (symmetricKey === null) {
3259
                typeOfKey = 'null';
3260
            }
3261
3262
            // in case of missing preparation, throw error
3263
            switch (typeOfKey) {
3264
                case 'string':
3265
                    // already set, all right
3266
                    return;
3267
                case 'null':
3268
                    // needs to be generated auto-generate
3269
                    symmetricKey = CryptTool.getSymmetricKey();
3270
                    break;
3271
                default:
3272
                    console.error('current invalid symmetricKey:', symmetricKey);
3273
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3274
            }
3275
            // password is optional
3276
        }
3277
3278
        /**
3279
         * called after successful upload
3280
         *
3281
         * @name   Uploader.success
3282
         * @private
3283
         * @function
3284
         * @param {int} status
3285
         * @param {int} result - optional
3286
         */
3287
        function success(status, result)
3288
        {
3289
            // add useful data to result
3290
            result.encryptionKey = symmetricKey;
3291
            result.requestData = data;
3292
3293
            if (successFunc !== null) {
3294
                successFunc(status, result);
3295
            }
3296
        }
3297
3298
        /**
3299
         * called after a upload failure
3300
         *
3301
         * @name   Uploader.fail
3302
         * @private
3303
         * @function
3304
         * @param {int} status - internal code
3305
         * @param {int} result - original error code
3306
         */
3307
        function fail(status, result)
3308
        {
3309
            if (failureFunc !== null) {
3310
                failureFunc(status, result);
3311
            }
3312
        }
3313
3314
        /**
3315
         * actually uploads the data
3316
         *
3317
         * @name   Uploader.run
3318
         * @function
3319
         */
3320
        me.run = function()
3321
        {
3322
            $.ajax({
3323
                type: 'POST',
3324
                url: url,
3325
                data: data,
3326
                dataType: 'json',
3327
                headers: ajaxHeaders,
3328
                success: function(result) {
3329
                    if (result.status === 0) {
3330
                        success(0, result);
3331
                    } else if (result.status === 1) {
3332
                        fail(1, result);
3333
                    } else {
3334
                        fail(2, result);
3335
                    }
3336
                }
3337
            })
3338
            .fail(function(jqXHR, textStatus, errorThrown) {
3339
                console.error(textStatus, errorThrown);
3340
                fail(3, jqXHR);
3341
            });
3342
        };
3343
3344
        /**
3345
         * set success function
3346
         *
3347
         * @name   Uploader.setUrl
3348
         * @function
3349
         * @param {function} newUrl
3350
         */
3351
        me.setUrl = function(newUrl)
3352
        {
3353
            url = newUrl;
3354
        };
3355
3356
        /**
3357
         * sets the password to use (first value) and optionally also the
3358
         * encryption key (not recommend, it is automatically generated).
3359
         *
3360
         * Note: Call this after prepare() as prepare() resets these values.
3361
         *
3362
         * @name   Uploader.setCryptValues
3363
         * @function
3364
         * @param {string} newPassword
3365
         * @param {string} newKey       - optional
3366
         */
3367
        me.setCryptParameters = function(newPassword, newKey)
3368
        {
3369
            password = newPassword;
3370
3371
            if (typeof newKey !== 'undefined') {
3372
                symmetricKey = newKey;
3373
            }
3374
        };
3375
3376
        /**
3377
         * set success function
3378
         *
3379
         * @name   Uploader.setSuccess
3380
         * @function
3381
         * @param {function} func
3382
         */
3383
        me.setSuccess = function(func)
3384
        {
3385
            successFunc = func;
3386
        };
3387
3388
        /**
3389
         * set failure function
3390
         *
3391
         * @name   Uploader.setFailure
3392
         * @function
3393
         * @param {function} func
3394
         */
3395
        me.setFailure = function(func)
3396
        {
3397
            failureFunc = func;
3398
        };
3399
3400
        /**
3401
         * prepares a new upload
3402
         *
3403
         * Call this when doing a new upload to reset any data from potential
3404
         * previous uploads. Must be called before any other method of this
3405
         * module.
3406
         *
3407
         * @name   Uploader.prepare
3408
         * @function
3409
         * @return {object}
3410
         */
3411
        me.prepare = function()
3412
        {
3413
            // entropy should already be checked!
3414
3415
            // reset password
3416
            password = '';
3417
3418
            // reset key, so it a new one is generated when it is used
3419
            symmetricKey = null;
3420
3421
            // reset data
3422
            successFunc = null;
3423
            failureFunc = null;
3424
            url = Helper.baseUri();
3425
            data = {};
3426
        };
3427
3428
        /**
3429
         * encrypts and sets the data
3430
         *
3431
         * @name   Uploader.setData
3432
         * @function
3433
         * @param {string} index
3434
         * @param {mixed} element
3435
         */
3436
        me.setData = function(index, element)
3437
        {
3438
            checkCryptParameters();
3439
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3440
        };
3441
3442
        /**
3443
         * set the additional metadata to send unencrypted
3444
         *
3445
         * @name   Uploader.setUnencryptedData
3446
         * @function
3447
         * @param {string} index
3448
         * @param {mixed} element
3449
         */
3450
        me.setUnencryptedData = function(index, element)
3451
        {
3452
            data[index] = element;
3453
        };
3454
3455
        /**
3456
         * set the additional metadata to send unencrypted passed at once
3457
         *
3458
         * @name   Uploader.setUnencryptedData
3459
         * @function
3460
         * @param {object} newData
3461
         */
3462
        me.setUnencryptedBulkData = function(newData)
3463
        {
3464
            $.extend(data, newData);
3465
        };
3466
3467
        /**
3468
         * Helper, which parses shows a general error message based on the result of the Uploader
3469
         *
3470
         * @name    Uploader.parseUploadError
3471
         * @function
3472
         * @param {int} status
3473
         * @param {object} data
3474
         * @param {string} doThisThing - a human description of the action, which was tried
3475
         * @return {array}
3476
         */
3477
        me.parseUploadError = function(status, data, doThisThing) {
3478
            var errorArray;
3479
3480
            switch (status) {
3481
                case me.error.custom:
0 ignored issues
show
The variable me seems to be never declared. If this is a global, consider adding a /** global: me */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
3482
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3483
                    break;
3484
                case me.error.unknown:
3485
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3486
                    break;
3487
                case me.error.serverError:
3488
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3489
                    break;
3490
                default:
3491
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3492
                    break;
3493
            }
3494
3495
            return errorArray;
3496
        };
3497
3498
        /**
3499
         * init Uploader
3500
         *
3501
         * @name   Uploader.init
3502
         * @function
3503
         */
3504
        me.init = function()
3505
        {
3506
            // nothing yet
3507
        };
3508
3509
        return me;
3510
    })();
3511
3512
    /**
3513
     * (controller) Responsible for encrypting paste and sending it to server.
3514
     *
3515
     * Does upload, encryption is done transparently by Uploader.
3516
     *
3517
     * @name PasteEncrypter
3518
     * @class
3519
     */
3520
    var PasteEncrypter = (function () {
3521
        var me = {};
3522
3523
        var requirementsChecked = false;
3524
3525
        /**
3526
         * checks whether there is a suitable amount of entrophy
3527
         *
3528
         * @name PasteEncrypter.checkRequirements
3529
         * @private
3530
         * @function
3531
         * @param {function} retryCallback - the callback to execute to retry the upload
3532
         * @return {bool}
3533
         */
3534
        function checkRequirements(retryCallback) {
3535
            // skip double requirement checks
3536
            if (requirementsChecked === true) {
3537
                return true;
3538
            }
3539
3540
            if (!CryptTool.isEntropyReady()) {
3541
                // display a message and wait
3542
                Alert.showStatus('Please move your mouse for more entropy…');
3543
3544
                CryptTool.addEntropySeedListener(retryCallback);
3545
                return false;
3546
            }
3547
3548
            requirementsChecked = true;
3549
3550
            return true;
3551
        }
3552
3553
        /**
3554
         * called after successful paste upload
3555
         *
3556
         * @name PasteEncrypter.showCreatedPaste
3557
         * @private
3558
         * @function
3559
         * @param {int} status
3560
         * @param {object} data
3561
         */
3562
        function showCreatedPaste(status, data) {
3563
            Alert.hideLoading();
3564
3565
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3566
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3567
3568
            Alert.hideMessages();
3569
3570
            // show notification
3571
            PasteStatus.createPasteNotification(url, deleteUrl);
3572
3573
            // show new URL in browser bar
3574
            history.pushState({type: 'newpaste'}, document.title, url);
3575
3576
            TopNav.showViewButtons();
3577
            TopNav.hideRawButton();
3578
            Editor.hide();
3579
3580
            // parse and show text
3581
            // (preparation already done in me.sendPaste())
3582
            PasteViewer.run();
3583
        }
3584
3585
        /**
3586
         * called after successful comment upload
3587
         *
3588
         * @name PasteEncrypter.showUploadedComment
3589
         * @private
3590
         * @function
3591
         * @param {int} status
3592
         * @param {object} data
3593
         */
3594
        function showUploadedComment(status, data) {
3595
            // show success message
3596
            Alert.showStatus('Comment posted.');
3597
3598
            // reload paste
3599
            Controller.refreshPaste(function () {
3600
                // highlight sent comment
3601
                DiscussionViewer.highlightComment(data.id, true);
3602
                // reset error handler
3603
                Alert.setCustomHandler(null);
3604
            });
3605
        }
3606
3607
        /**
3608
         * adds attachments to the Uploader
3609
         *
3610
         * @name PasteEncrypter.encryptAttachments
3611
         * @private
3612
         * @function
3613
         * @param {function} callback - excuted when action is successful
3614
         */
3615
        function encryptAttachments(callback) {
3616
            var file = AttachmentViewer.getAttachmentData();
3617
3618
            if (typeof file !== 'undefined' && file !== null) {
3619
                var fileName = AttachmentViewer.getFile().name;
3620
3621
                Uploader.setData('attachment', file);
3622
                Uploader.setData('attachmentname', fileName);
3623
3624
                // run callback
3625
                return callback();
3626
            } else if (AttachmentViewer.hasAttachment()) {
3627
                // fall back to cloned part
3628
                var attachment = AttachmentViewer.getAttachment();
3629
3630
                Uploader.setData('attachment', attachment[0]);
3631
                Uploader.setData('attachmentname', attachment[1]);
3632
                return callback();
3633
            } else {
3634
                // if there are no attachments, this is of course still successful
3635
                return callback();
3636
            }
3637
        }
3638
3639
        /**
3640
         * send a reply in a discussion
3641
         *
3642
         * @name   PasteEncrypter.sendComment
3643
         * @function
3644
         */
3645
        me.sendComment = function()
3646
        {
3647
            Alert.hideMessages();
3648
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3649
3650
            // UI loading state
3651
            TopNav.hideAllButtons();
3652
            Alert.showLoading('Sending comment…', 'cloud-upload');
3653
3654
            // get data
3655
            var plainText = DiscussionViewer.getReplyMessage(),
3656
                nickname = DiscussionViewer.getReplyNickname(),
3657
                parentid = DiscussionViewer.getReplyCommentId();
3658
3659
            // do not send if there is no data
3660
            if (plainText.length === 0) {
3661
                // revert loading status…
3662
                Alert.hideLoading();
3663
                Alert.setCustomHandler(null);
3664
                TopNav.showViewButtons();
3665
                return;
3666
            }
3667
3668
            // check entropy
3669
            if (!checkRequirements(function () {
3670
                me.sendComment();
3671
            })) {
3672
                return; // to prevent multiple executions
3673
            }
3674
3675
            // prepare Uploader
3676
            Uploader.prepare();
3677
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3678
3679
            // set success/fail functions
3680
            Uploader.setSuccess(showUploadedComment);
3681
            Uploader.setFailure(function (status, data) {
3682
                // revert loading status…
3683
                Alert.hideLoading();
3684
                TopNav.showViewButtons();
3685
3686
                // show error message
3687
                Alert.showError(
3688
                    Uploader.parseUploadError(status, data, 'post comment')
3689
                );
3690
3691
                // reset error handler
3692
                Alert.setCustomHandler(null);
3693
            });
3694
3695
            // fill it with unencrypted params
3696
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3697
            if (typeof parentid === 'undefined') {
3698
                // if parent id is not set, this is the top-most comment, so use
3699
                // paste id as parent, as the root element of the discussion tree
3700
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3701
            } else {
3702
                Uploader.setUnencryptedData('parentid', parentid);
3703
            }
3704
3705
            // encrypt data
3706
            Uploader.setData('data', plainText);
3707
3708
            if (nickname.length > 0) {
3709
                Uploader.setData('nickname', nickname);
3710
            }
3711
3712
            Uploader.run();
3713
        };
3714
3715
        /**
3716
         * sends a new paste to server
3717
         *
3718
         * @name   PasteEncrypter.sendPaste
3719
         * @function
3720
         */
3721
        me.sendPaste = function()
3722
        {
3723
            // hide previous (error) messages
3724
            Controller.hideStatusMessages();
3725
3726
            // UI loading state
3727
            TopNav.hideAllButtons();
3728
            Alert.showLoading('Sending paste…', 'cloud-upload');
3729
            TopNav.collapseBar();
3730
3731
            // get data
3732
            var plainText = Editor.getText(),
3733
                format = PasteViewer.getFormat(),
3734
                // the methods may return different values if no files are attached (null, undefined or false)
3735
                files = TopNav.getFileList() || AttachmentViewer.getFile() || AttachmentViewer.hasAttachment();
3736
3737
            // do not send if there is no data
3738
            if (plainText.length === 0 && !files) {
3739
                // revert loading status…
3740
                Alert.hideLoading();
3741
                TopNav.showCreateButtons();
3742
                return;
3743
            }
3744
3745
            // check entropy
3746
            if (!checkRequirements(function () {
3747
                me.sendPaste();
3748
            })) {
3749
                return; // to prevent multiple executions
3750
            }
3751
3752
            // prepare Uploader
3753
            Uploader.prepare();
3754
            Uploader.setCryptParameters(TopNav.getPassword());
3755
3756
            // set success/fail functions
3757
            Uploader.setSuccess(showCreatedPaste);
3758
            Uploader.setFailure(function (status, data) {
3759
                // revert loading status…
3760
                Alert.hideLoading();
3761
                TopNav.showCreateButtons();
3762
3763
                // show error message
3764
                Alert.showError(
3765
                    Uploader.parseUploadError(status, data, 'create paste')
3766
                );
3767
            });
3768
3769
            // fill it with unencrypted submitted options
3770
            Uploader.setUnencryptedBulkData({
3771
                expire:           TopNav.getExpiration(),
3772
                formatter:        format,
3773
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3774
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3775
            });
3776
3777
            // prepare PasteViewer for later preview
3778
            PasteViewer.setText(plainText);
3779
            PasteViewer.setFormat(format);
3780
3781
            // encrypt cipher data
3782
            Uploader.setData('data', plainText);
3783
3784
            // encrypt attachments
3785
            encryptAttachments(
3786
                function () {
3787
                    // send data
3788
                    Uploader.run();
3789
                }
3790
            );
3791
        };
3792
3793
        /**
3794
         * initialize
3795
         *
3796
         * @name   PasteEncrypter.init
3797
         * @function
3798
         */
3799
        me.init = function()
3800
        {
3801
            // nothing yet
3802
        };
3803
3804
        return me;
3805
    })();
3806
3807
    /**
3808
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3809
     *
3810
     * Only decryption, no download.
3811
     *
3812
     * @name PasteDecrypter
3813
     * @class
3814
     */
3815
    var PasteDecrypter = (function () {
3816
        var me = {};
3817
3818
        /**
3819
         * decrypt data or prompts for password in cvase of failure
3820
         *
3821
         * @name   PasteDecrypter.decryptOrPromptPassword
3822
         * @private
3823
         * @function
3824
         * @param  {string} key
3825
         * @param  {string} password - optional, may be an empty string
3826
         * @param  {string} cipherdata
3827
         * @throws {string}
3828
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3829
         */
3830
        function decryptOrPromptPassword(key, password, cipherdata)
3831
        {
3832
            // try decryption without password
3833
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3834
3835
            // if it fails, request password
3836
            if (plaindata.length === 0 && password.length === 0) {
3837
                // try to get cached password first
3838
                password = Prompt.getPassword();
3839
3840
                // if password is there, re-try
3841
                if (password.length === 0) {
3842
                    password = Prompt.requestPassword();
3843
                }
3844
                // recursive
3845
                // note: an infinite loop is prevented as the previous if
3846
                // clause checks whether a password is already set and ignores
3847
                // errors when a password has been passed
3848
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3849
            }
3850
3851
            // if all tries failed, we can only return an error
3852
            if (plaindata.length === 0) {
3853
                throw 'failed to decipher data';
3854
            }
3855
3856
            return plaindata;
3857
        }
3858
3859
        /**
3860
         * decrypt the actual paste text
3861
         *
3862
         * @name   PasteDecrypter.decryptOrPromptPassword
3863
         * @private
3864
         * @function
3865
         * @param  {object} paste - paste data in object form
3866
         * @param  {string} key
3867
         * @param  {string} password
3868
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3869
         * @return {bool} whether action was successful
3870
         * @throws {string}
3871
         */
3872
        function decryptPaste(paste, key, password, ignoreError)
3873
        {
3874
            var plaintext;
3875
            if (ignoreError === true) {
3876
                plaintext = CryptTool.decipher(key, password, paste.data);
3877
            } else {
3878
                try {
3879
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3880
                } catch (err) {
3881
                    throw 'failed to decipher paste text: ' + err;
3882
                }
3883
                if (plaintext === false) {
3884
                    return false;
3885
                }
3886
            }
3887
3888
            // on success show paste
3889
            PasteViewer.setFormat(paste.meta.formatter);
3890
            PasteViewer.setText(plaintext);
3891
            // trigger to show the text (attachment loaded afterwards)
3892
            PasteViewer.run();
3893
3894
            return true;
3895
        }
3896
3897
        /**
3898
         * decrypts any attachment
3899
         *
3900
         * @name   PasteDecrypter.decryptAttachment
3901
         * @private
3902
         * @function
3903
         * @param  {object} paste - paste data in object form
3904
         * @param  {string} key
3905
         * @param  {string} password
3906
         * @return {bool} whether action was successful
3907
         * @throws {string}
3908
         */
3909
        function decryptAttachment(paste, key, password)
3910
        {
3911
            var attachment, attachmentName;
3912
3913
            // decrypt attachment
3914
            try {
3915
                attachment = decryptOrPromptPassword(key, password, paste.attachment);
3916
            } catch (err) {
3917
                throw 'failed to decipher attachment: ' + err;
3918
            }
3919
            if (attachment === false) {
3920
                return false;
3921
            }
3922
3923
            // decrypt attachment name
3924
            if (paste.attachmentname) {
3925
                try {
3926
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3927
                } catch (err) {
3928
                    throw 'failed to decipher attachment name: ' + err;
3929
                }
3930
                if (attachmentName === false) {
3931
                    return false;
3932
                }
3933
            }
3934
3935
            AttachmentViewer.setAttachment(attachment, attachmentName);
3936
            AttachmentViewer.showAttachment();
3937
3938
            return true;
3939
        }
3940
3941
        /**
3942
         * decrypts all comments and shows them
3943
         *
3944
         * @name   PasteDecrypter.decryptComments
3945
         * @private
3946
         * @function
3947
         * @param  {object} paste - paste data in object form
3948
         * @param  {string} key
3949
         * @param  {string} password
3950
         * @return {bool} whether action was successful
3951
         */
3952
        function decryptComments(paste, key, password)
3953
        {
3954
            // remove potentially previous discussion
3955
            DiscussionViewer.prepareNewDiscussion();
3956
3957
            // iterate over comments
3958
            for (var i = 0; i < paste.comments.length; ++i) {
3959
                var comment = paste.comments[i];
3960
3961
                DiscussionViewer.addComment(
3962
                    comment,
3963
                    CryptTool.decipher(key, password, comment.data),
3964
                    CryptTool.decipher(key, password, comment.meta.nickname)
3965
                );
3966
            }
3967
3968
            DiscussionViewer.finishDiscussion();
3969
            return true;
3970
        }
3971
3972
        /**
3973
         * show decrypted text in the display area, including discussion (if open)
3974
         *
3975
         * @name   PasteDecrypter.run
3976
         * @function
3977
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3978
         */
3979
        me.run = function(paste)
3980
        {
3981
            Alert.hideMessages();
3982
            Alert.showLoading('Decrypting paste…', 'cloud-download');
3983
3984
            if (typeof paste === 'undefined') {
3985
                paste = $.parseJSON(Model.getCipherData());
3986
            }
3987
3988
            var key = Model.getPasteKey(),
3989
                password = Prompt.getPassword();
3990
3991
            if (PasteViewer.isPrettyPrinted()) {
3992
                // don't decrypt twice
3993
                return;
3994
            }
3995
3996
            // try to decrypt the paste
3997
            try {
3998
                // decrypt attachments
3999
                if (paste.attachment) {
4000
                    if (AttachmentViewer.hasAttachmentData()) {
4001
                        // try to decrypt paste and if it fails (because the password is
4002
                        // missing) return to let JS continue and wait for user
4003
                        if (!decryptAttachment(paste, key, password)) {
4004
                            return;
4005
                        }
4006
                    }
4007
                    // ignore empty paste, as this is allowed when pasting attachments
4008
                    decryptPaste(paste, key, password, true);
4009
                } else {
4010
                    decryptPaste(paste, key, password);
4011
                }
4012
4013
4014
                // shows the remaining time (until) deletion
4015
                PasteStatus.showRemainingTime(paste.meta);
4016
4017
                // if the discussion is opened on this paste, display it
4018
                if (paste.meta.opendiscussion) {
4019
                    decryptComments(paste, key, password);
4020
                }
4021
4022
                Alert.hideLoading();
4023
                TopNav.showViewButtons();
4024
            } catch(err) {
4025
                Alert.hideLoading();
4026
4027
                // log and show error
4028
                console.error(err);
4029
                Alert.showError('Could not decrypt data (Wrong key?)');
4030
            }
4031
        };
4032
4033
        /**
4034
         * initialize
4035
         *
4036
         * @name   PasteDecrypter.init
4037
         * @function
4038
         */
4039
        me.init = function()
4040
        {
4041
            // nothing yet
4042
        };
4043
4044
        return me;
4045
    })();
4046
4047
    /**
4048
     * (controller) main PrivateBin logic
4049
     *
4050
     * @name   Controller
4051
     * @param  {object} window
4052
     * @param  {object} document
4053
     * @class
4054
     */
4055
    var Controller = (function (window, document) {
4056
        var me = {};
4057
4058
        /**
4059
         * hides all status messages no matter which module showed them
4060
         *
4061
         * @name   Controller.hideStatusMessages
4062
         * @function
4063
         */
4064
        me.hideStatusMessages = function()
4065
        {
4066
            PasteStatus.hideMessages();
4067
            Alert.hideMessages();
4068
        };
4069
4070
        /**
4071
         * creates a new paste
4072
         *
4073
         * @name   Controller.newPaste
4074
         * @function
4075
         */
4076
        me.newPaste = function()
4077
        {
4078
            // Important: This *must not* run Alert.hideMessages() as previous
4079
            // errors from viewing a paste should be shown.
4080
            TopNav.hideAllButtons();
4081
            Alert.showLoading('Preparing new paste…', 'time');
4082
4083
            PasteStatus.hideMessages();
4084
            PasteViewer.hide();
4085
            Editor.resetInput();
4086
            Editor.show();
4087
            Editor.focusInput();
4088
            AttachmentViewer.removeAttachment();
4089
4090
            TopNav.showCreateButtons();
4091
            Alert.hideLoading();
4092
        };
4093
4094
        /**
4095
         * shows the loaded paste
4096
         *
4097
         * @name   Controller.showPaste
4098
         * @function
4099
         */
4100
        me.showPaste = function()
4101
        {
4102
            try {
4103
                Model.getPasteId();
4104
                Model.getPasteKey();
4105
            } catch (err) {
4106
                console.error(err);
4107
4108
                // missing decryption key (or paste ID) in URL?
4109
                if (window.location.hash.length === 0) {
4110
                    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?)');
4111
                    return;
4112
                }
4113
            }
4114
4115
            // show proper elements on screen
4116
            PasteDecrypter.run();
4117
        };
4118
4119
        /**
4120
         * refreshes the loaded paste to show potential new data
4121
         *
4122
         * @name   Controller.refreshPaste
4123
         * @function
4124
         * @param  {function} callback
4125
         */
4126
        me.refreshPaste = function(callback)
4127
        {
4128
            // save window position to restore it later
4129
            var orgPosition = $(window).scrollTop();
4130
4131
            Uploader.prepare();
4132
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
4133
4134
            Uploader.setFailure(function (status, data) {
4135
                // revert loading status…
4136
                Alert.hideLoading();
4137
                TopNav.showViewButtons();
4138
4139
                // show error message
4140
                Alert.showError(
4141
                    Uploader.parseUploadError(status, data, 'refresh display')
4142
                );
4143
            });
4144
            Uploader.setSuccess(function (status, data) {
4145
                PasteDecrypter.run(data);
4146
4147
                // restore position
4148
                window.scrollTo(0, orgPosition);
4149
4150
                callback();
4151
            });
4152
            Uploader.run();
4153
        };
4154
4155
        /**
4156
         * clone the current paste
4157
         *
4158
         * @name   Controller.clonePaste
4159
         * @function
4160
         */
4161
        me.clonePaste = function()
4162
        {
4163
            TopNav.collapseBar();
4164
            TopNav.hideAllButtons();
4165
            Alert.showLoading('Cloning paste…', 'transfer');
4166
4167
            // hide messages from previous paste
4168
            me.hideStatusMessages();
4169
4170
            // erase the id and the key in url
4171
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
4172
4173
            if (AttachmentViewer.hasAttachment()) {
4174
                AttachmentViewer.moveAttachmentTo(
4175
                    TopNav.getCustomAttachment(),
4176
                    'Cloned: \'%s\''
4177
                );
4178
                TopNav.hideFileSelector();
4179
                AttachmentViewer.hideAttachment();
4180
                // NOTE: it also looks nice without removing the attachment
4181
                // but for a consistent display we remove it…
4182
                AttachmentViewer.hideAttachmentPreview();
4183
                TopNav.showCustomAttachment();
4184
4185
                // show another status message to make the user aware that the
4186
                // file was cloned too!
4187
                Alert.showStatus(
4188
                    [
4189
                        'The cloned file \'%s\' was attached to this paste.',
4190
                        AttachmentViewer.getAttachment()[1]
4191
                    ],
4192
                    'copy'
4193
                );
4194
            }
4195
4196
            Editor.setText(PasteViewer.getText());
4197
            PasteViewer.hide();
4198
            Editor.show();
4199
4200
            Alert.hideLoading();
4201
            TopNav.showCreateButtons();
4202
        };
4203
4204
        /**
4205
         * removes a saved paste
4206
         *
4207
         * @name   Controller.removePaste
4208
         * @function
4209
         * @param  {string} pasteId
4210
         * @param  {string} deleteToken
4211
         */
4212
        me.removePaste = function(pasteId, deleteToken) {
4213
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
4214
            // so we use a POST request
4215
            Uploader.prepare();
4216
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
4217
            Uploader.setUnencryptedData('deletetoken', deleteToken);
4218
4219
            Uploader.setFailure(function () {
4220
                Alert.showError(
4221
                    I18n._('Could not delete the paste, it was not stored in burn after reading mode.')
4222
                );
4223
            });
4224
            Uploader.run();
4225
        };
4226
4227
        /**
4228
         * application start
4229
         *
4230
         * @name   Controller.init
4231
         * @function
4232
         */
4233
        me.init = function()
4234
        {
4235
            // first load translations
4236
            I18n.loadTranslations();
4237
4238
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
4239
4240
            // initialize other modules/"classes"
4241
            Alert.init();
4242
            Model.init();
4243
            AttachmentViewer.init();
4244
            DiscussionViewer.init();
4245
            Editor.init();
4246
            PasteDecrypter.init();
4247
            PasteEncrypter.init();
4248
            PasteStatus.init();
4249
            PasteViewer.init();
4250
            Prompt.init();
4251
            TopNav.init();
4252
            UiHelper.init();
4253
            Uploader.init();
4254
4255
            // display an existing paste
4256
            if (Model.hasCipherData()) {
4257
                return me.showPaste();
4258
            }
4259
4260
            // otherwise create a new paste
4261
            me.newPaste();
4262
        };
4263
4264
        return me;
4265
    })(window, document);
4266
4267
    return {
4268
        Helper: Helper,
4269
        I18n: I18n,
4270
        CryptTool: CryptTool,
4271
        Model: Model,
4272
        UiHelper: UiHelper,
4273
        Alert: Alert,
4274
        PasteStatus: PasteStatus,
4275
        Prompt: Prompt,
4276
        Editor: Editor,
4277
        PasteViewer: PasteViewer,
4278
        AttachmentViewer: AttachmentViewer,
4279
        DiscussionViewer: DiscussionViewer,
4280
        TopNav: TopNav,
4281
        Uploader: Uploader,
4282
        PasteEncrypter: PasteEncrypter,
4283
        PasteDecrypter: PasteDecrypter,
4284
        Controller: Controller
4285
    };
4286
})(jQuery, sjcl, Base64, RawDeflate);
4287