Passed
Push — master ( 98d07e...ffae61 )
by El
03:13
created

js/privatebin.js (1 issue)

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