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

me.handleAttachmentPreview   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
c 0
b 0
f 0
nc 7
nop 2
dl 0
loc 63
rs 7.2689

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
Documentation introduced by
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
Documentation introduced by
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
0 ignored issues
show
Documentation introduced by
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
291
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
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
Documentation introduced by
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
0 ignored issues
show
Documentation introduced by
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
313
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
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
Documentation introduced by
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) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if data !== undefined is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

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