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

privatebin.js ➔ ... ➔ $(document).paste   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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