Passed
Push — master ( 57754f...4ccb4b )
by El
03:38
created

me.run   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
4292
                Uploader.prepare();
4293
                Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
4294
4295
                Uploader.setFailure(function (status, data) {
4296
                    // revert loading status…
4297
                    Alert.hideLoading();
4298
                    TopNav.showViewButtons();
4299
4300
                    // show error message
4301
                    Alert.showError(
4302
                        Uploader.parseUploadError(status, data, 'refresh display')
4303
                    );
4304
                });
4305
                Uploader.setSuccess(function (status, data) {
4306
                    PasteDecrypter.run(data);
4307
4308
                    // restore position
4309
                    window.scrollTo(0, orgPosition);
4310
4311
                    PasteDecrypter.run(data);
4312
4313
                    // NOTE: could create problems as callback may be called
4314
                    // asyncronously if PasteDecrypter e.g. needs to wait for a
4315
                    // password being entered
4316
                    callback();
4317
                });
4318
                Uploader.run();
4319
            }, false); // this false is important as it circumvents the cache
4320
        }
4321
4322
        /**
4323
         * clone the current paste
4324
         *
4325
         * @name   Controller.clonePaste
4326
         * @function
4327
         */
4328
        me.clonePaste = function()
4329
        {
4330
            TopNav.collapseBar();
4331
            TopNav.hideAllButtons();
4332
            Alert.showLoading('Cloning paste…', 'transfer');
4333
4334
            // hide messages from previous paste
4335
            me.hideStatusMessages();
4336
4337
            // erase the id and the key in url
4338
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
4339
4340
            if (AttachmentViewer.hasAttachment()) {
4341
                AttachmentViewer.moveAttachmentTo(
4342
                    TopNav.getCustomAttachment(),
4343
                    'Cloned: \'%s\''
4344
                );
4345
                TopNav.hideFileSelector();
4346
                AttachmentViewer.hideAttachment();
4347
                // NOTE: it also looks nice without removing the attachment
4348
                // but for a consistent display we remove it…
4349
                AttachmentViewer.hideAttachmentPreview();
4350
                TopNav.showCustomAttachment();
4351
4352
                // show another status message to make the user aware that the
4353
                // file was cloned too!
4354
                Alert.showStatus(
4355
                    [
4356
                        'The cloned file \'%s\' was attached to this paste.',
4357
                        AttachmentViewer.getAttachment()[1]
4358
                    ],
4359
                    'copy'
4360
                );
4361
            }
4362
4363
            Editor.setText(PasteViewer.getText());
4364
            PasteViewer.hide();
4365
            Editor.show();
4366
4367
            Alert.hideLoading();
4368
            TopNav.showCreateButtons();
4369
        };
4370
4371
        /**
4372
         * removes a saved paste
4373
         *
4374
         * @name   Controller.removePaste
4375
         * @function
4376
         * @param  {string} pasteId
4377
         * @param  {string} deleteToken
4378
         * @deprecated not used anymore, de we still need it?
4379
         */
4380
        me.removePaste = function(pasteId, deleteToken) {
4381
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
4382
            // so we use a POST request
4383
            Uploader.prepare();
4384
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
4385
            Uploader.setUnencryptedData('deletetoken', deleteToken);
4386
4387
            Uploader.setFailure(function () {
4388
                Alert.showError(
4389
                    I18n._('Could not delete the paste, it was not stored in burn after reading mode.')
4390
                );
4391
            });
4392
            Uploader.run();
4393
        };
4394
4395
        /**
4396
         * application start
4397
         *
4398
         * @name   Controller.init
4399
         * @function
4400
         */
4401
        me.init = function()
4402
        {
4403
            // first load translations
4404
            I18n.loadTranslations();
4405
4406
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
4407
4408
            // initialize other modules/"classes"
4409
            Alert.init();
4410
            Model.init();
4411
            AttachmentViewer.init();
4412
            DiscussionViewer.init();
4413
            Editor.init();
4414
            PasteDecrypter.init();
4415
            PasteEncrypter.init();
4416
            PasteStatus.init();
4417
            PasteViewer.init();
4418
            Prompt.init();
4419
            TopNav.init();
4420
            UiHelper.init();
4421
            Uploader.init();
4422
4423
            // check whether existing paste needs to be shown
4424
            try {
4425
                Model.getPasteId();
4426
            } catch (e) {
4427
                // otherwise create a new paste
4428
                return me.newPaste();
4429
            }
4430
4431
            // if delete token is passed (i.e. paste has been deleted by this access)
4432
            // there is no more stuf we need to do
4433
            if (Model.hasDeleteToken()) {
4434
                return;
4435
            }
4436
4437
            // prevent bots from viewing a paste and potentially deleting data
4438
            // when burn-after-reading is set
4439
            // see https://github.com/elrido/ZeroBin/issues/11
4440
            if (Helper.isBadBot()) {
4441
                return me.showBadBotMessage();
4442
            }
4443
4444
            // display an existing paste
4445
            return me.showPaste();
4446
        }
4447
4448
        return me;
4449
    })(window, document);
4450
4451
    return {
4452
        Helper: Helper,
4453
        I18n: I18n,
4454
        CryptTool: CryptTool,
4455
        Model: Model,
4456
        UiHelper: UiHelper,
4457
        Alert: Alert,
4458
        PasteStatus: PasteStatus,
4459
        Prompt: Prompt,
4460
        Editor: Editor,
4461
        PasteViewer: PasteViewer,
4462
        AttachmentViewer: AttachmentViewer,
4463
        DiscussionViewer: DiscussionViewer,
4464
        TopNav: TopNav,
4465
        Uploader: Uploader,
4466
        PasteEncrypter: PasteEncrypter,
4467
        PasteDecrypter: PasteDecrypter,
4468
        Controller: Controller
4469
    };
4470
})(jQuery, sjcl, Base64, RawDeflate);
4471