Passed
Push — master ( 76c147...6eb882 )
by El
04:25 queued 01:03
created

js/privatebin.js (1 issue)

Severity
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
561
         */
562
        me.decipher = function(key, password, data)
563
        {
564
            if (data !== undefined) {
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
                        // ignore error, because ????? @TODO
572
                    }
573
                }
574
            }
575
            return '';
576
        }
577
578
        /**
579
         * checks whether the crypt tool has collected enough entropy
580
         *
581
         * @name   CryptTool.isEntropyReady
582
         * @function
583
         * @return {bool}
584
         */
585
        me.isEntropyReady = function()
586
        {
587
            return sjcl.random.isReady();
588
        }
589
590
        /**
591
         * add a listener function, triggered when enough entropy is available
592
         *
593
         * @name   CryptTool.addEntropySeedListener
594
         * @function
595
         * @param {function} func
596
         */
597
        me.addEntropySeedListener = function(func)
598
        {
599
            sjcl.random.addEventListener('seeded', func);
600
        }
601
602
        /**
603
         * returns a random symmetric key
604
         *
605
         * @name   CryptTool.getSymmetricKey
606
         * @function
607
         * @return {string} func
608
         */
609
        me.getSymmetricKey = function()
610
        {
611
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
612
        }
613
614
        return me;
615
    })();
616
617
    /**
618
     * (Model) Data source (aka MVC)
619
     *
620
     * @name   Model
621
     * @class
622
     */
623
    var Model = (function () {
624
        var me = {};
625
626
        var $cipherData,
627
            $templates;
628
629
        var id = null, symmetricKey = null;
630
631
        /**
632
         * returns the expiration set in the HTML
633
         *
634
         * @name   Model.getExpirationDefault
635
         * @function
636
         * @return string
637
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
638
         */
639
        me.getExpirationDefault = function()
640
        {
641
            return $('#pasteExpiration').val();
642
        }
643
644
        /**
645
         * returns the format set in the HTML
646
         *
647
         * @name   Model.getFormatDefault
648
         * @function
649
         * @return string
650
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
651
         */
652
        me.getFormatDefault = function()
653
        {
654
            return $('#pasteFormatter').val();
655
        }
656
657
        /**
658
         * check if cipher data was supplied
659
         *
660
         * @name   Model.getCipherData
661
         * @function
662
         * @return boolean
663
         */
664
        me.hasCipherData = function()
665
        {
666
            return (me.getCipherData().length > 0);
667
        }
668
669
        /**
670
         * returns the cipher data
671
         *
672
         * @name   Model.getCipherData
673
         * @function
674
         * @return string
675
         */
676
        me.getCipherData = function()
677
        {
678
            return $cipherData.text();
679
        }
680
681
        /**
682
         * get the pastes unique identifier from the URL,
683
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
684
         *
685
         * @name   Model.getPasteId
686
         * @function
687
         * @return {string} unique identifier
688
         * @throws {string}
689
         */
690
        me.getPasteId = function()
691
        {
692
            if (id === null) {
693
                id = window.location.search.substring(1);
694
695
                if (id === '') {
696
                    throw 'no paste id given';
697
                }
698
            }
699
700
            return id;
701
        }
702
703
        /**
704
         * return the deciphering key stored in anchor part of the URL
705
         *
706
         * @name   Model.getPasteKey
707
         * @function
708
         * @return {string|null} key
709
         * @throws {string}
710
         */
711
        me.getPasteKey = function()
712
        {
713
            if (symmetricKey === null) {
714
                symmetricKey = window.location.hash.substring(1);
715
716
                if (symmetricKey === '') {
717
                    throw 'no encryption key given';
718
                }
719
720
                // Some web 2.0 services and redirectors add data AFTER the anchor
721
                // (such as &utm_source=...). We will strip any additional data.
722
                var ampersandPos = symmetricKey.indexOf('&');
723
                if (ampersandPos > -1)
724
                {
725
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
726
                }
727
            }
728
729
            return symmetricKey;
730
        }
731
732
        /**
733
         * returns a jQuery copy of the HTML template
734
         *
735
         * @name Model.getTemplate
736
         * @function
737
         * @param  {string} name - the name of the template
738
         * @return {jQuery}
739
         */
740
        me.getTemplate = function(name)
741
        {
742
            // find template
743
            var $element = $templates.find('#' + name + 'template').clone(true);
744
            // change ID to avoid collisions (one ID should really be unique)
745
            return $element.prop('id', name);
746
        }
747
748
        /**
749
         * resets state, used for unit testing
750
         *
751
         * @name   Model.reset
752
         * @function
753
         */
754
        me.reset = function()
755
        {
756
            $cipherData = $templates = id = symmetricKey = null;
757
        }
758
759
        /**
760
         * init navigation manager
761
         *
762
         * preloads jQuery elements
763
         *
764
         * @name   Model.init
765
         * @function
766
         */
767
        me.init = function()
768
        {
769
            $cipherData = $('#cipherdata');
770
            $templates = $('#templates');
771
        }
772
773
        return me;
774
    })();
775
776
    /**
777
     * Helper functions for user interface
778
     *
779
     * everything directly UI-related, which fits nowhere else
780
     *
781
     * @name   UiHelper
782
     * @class
783
     */
784
    var UiHelper = (function () {
785
        var me = {};
786
787
        /**
788
         * handle history (pop) state changes
789
         *
790
         * currently this does only handle redirects to the home page.
791
         *
792
         * @name   UiHelper.historyChange
793
         * @private
794
         * @function
795
         * @param  {Event} event
796
         */
797
        function historyChange(event)
798
        {
799
            var currentLocation = Helper.baseUri();
800
            if (event.originalEvent.state === null && // no state object passed
801
                event.target.location.href === currentLocation && // target location is home page
802
                window.location.href === currentLocation // and we are not already on the home page
803
            ) {
804
                // redirect to home page
805
                window.location.href = currentLocation;
806
            }
807
        }
808
809
        /**
810
         * reload the page
811
         *
812
         * This takes the user to the PrivateBin homepage.
813
         *
814
         * @name   UiHelper.reloadHome
815
         * @function
816
         */
817
        me.reloadHome = function()
818
        {
819
            window.location.href = Helper.baseUri();
820
        }
821
822
        /**
823
         * checks whether the element is currently visible in the viewport (so
824
         * the user can actually see it)
825
         *
826
         * @see    {@link https://stackoverflow.com/a/40658647}
827
         * @name   UiHelper.isVisible
828
         * @function
829
         * @param  {jQuery} $element The link hash to move to.
830
         */
831
        me.isVisible = function($element)
832
        {
833
            var elementTop = $element.offset().top;
834
            var viewportTop = $(window).scrollTop();
835
            var viewportBottom = viewportTop + $(window).height();
836
837
            return (elementTop > viewportTop && elementTop < viewportBottom);
838
        }
839
840
        /**
841
         * scrolls to a specific element
842
         *
843
         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
844
         * @name   UiHelper.scrollTo
845
         * @function
846
         * @param  {jQuery}           $element        The link hash to move to.
847
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
848
         * @param  {string}           animationEffect   passed to jQuery .animate
849
         * @param  {function}         finishedCallback  function to call after animation finished
850
         */
851
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
852
        {
853
            var $body = $('html, body'),
854
                margin = 50,
855
                callbackCalled = false;
856
857
            //calculate destination place
858
            var dest = 0;
859
            // if it would scroll out of the screen at the bottom only scroll it as
860
            // far as the screen can go
861
            if ($element.offset().top > $(document).height() - $(window).height()) {
862
                dest = $(document).height() - $(window).height();
863
            } else {
864
                dest = $element.offset().top - margin;
865
            }
866
            // skip animation if duration is set to 0
867
            if (animationDuration === 0) {
868
                window.scrollTo(0, dest);
869
            } else {
870
                // stop previous animation
871
                $body.stop();
872
                // scroll to destination
873
                $body.animate({
874
                    scrollTop: dest
875
                }, animationDuration, animationEffect);
876
            }
877
878
            // as we have finished we can enable scrolling again
879
            $body.queue(function (next) {
880
                if (!callbackCalled) {
881
                    // call user function if needed
882
                    if (typeof finishedCallback !== 'undefined') {
883
                        finishedCallback();
884
                    }
885
886
                    // prevent calling this function twice
887
                    callbackCalled = true;
888
                }
889
                next();
890
            });
891
        }
892
893
        /**
894
         * trigger a history (pop) state change
895
         *
896
         * used to test the UiHelper.historyChange private function
897
         *
898
         * @name   UiHelper.mockHistoryChange
899
         * @function
900
         * @param  {string} state   (optional) state to mock
901
         */
902
        me.mockHistoryChange = function(state)
903
        {
904
            if (typeof state === 'undefined') {
905
                state = null;
906
            }
907
            historyChange($.Event('popstate', {originalEvent: new PopStateEvent('popstate', {state: state}), target: window}));
908
        }
909
910
        /**
911
         * initialize
912
         *
913
         * @name   UiHelper.init
914
         * @function
915
         */
916
        me.init = function()
917
        {
918
            // update link to home page
919
            $('.reloadlink').prop('href', Helper.baseUri());
920
921
            $(window).on('popstate', historyChange);
922
        }
923
924
        return me;
925
    })();
926
927
    /**
928
     * Alert/error manager
929
     *
930
     * @name   Alert
931
     * @class
932
     */
933
    var Alert = (function () {
934
        var me = {};
935
936
        var $errorMessage,
937
            $loadingIndicator,
938
            $statusMessage,
939
            $remainingTime;
940
941
        var currentIcon;
942
943
        var alertType = [
944
            'loading', // not in bootstrap, but using a good value here
945
            'info', // status icon
946
            'warning', // not used yet
947
            'danger' // error icon
948
        ];
949
950
        var customHandler;
951
952
        /**
953
         * forwards a request to the i18n module and shows the element
954
         *
955
         * @name   Alert.handleNotification
956
         * @private
957
         * @function
958
         * @param  {int} id - id of notification
959
         * @param  {jQuery} $element - jQuery object
960
         * @param  {string|array} args
961
         * @param  {string|null} icon - optional, icon
962
         */
963
        function handleNotification(id, $element, args, icon)
964
        {
965
            // basic parsing/conversion of parameters
966
            if (typeof icon === 'undefined') {
967
                icon = null;
968
            }
969
            if (typeof args === 'undefined') {
970
                args = null;
971
            } else if (typeof args === 'string') {
972
                // convert string to array if needed
973
                args = [args];
974
            }
975
976
            // pass to custom handler if defined
977
            if (typeof customHandler === 'function') {
978
                var handlerResult = customHandler(alertType[id], $element, args, icon);
979
                if (handlerResult === true) {
980
                    // if it returs true, skip own handler
981
                    return;
982
                }
983
                if (handlerResult instanceof jQuery) {
984
                    // continue processing with new element
985
                    $element = handlerResult;
986
                    icon = null; // icons not supported in this case
987
                }
988
            }
989
990
            // handle icon
991
            if (icon !== null && // icon was passed
992
                icon !== currentIcon[id] // and it differs from current icon
993
            ) {
994
                var $glyphIcon = $element.find(':first');
995
996
                // remove (previous) icon
997
                $glyphIcon.removeClass(currentIcon[id]);
998
999
                // any other thing as a string (e.g. 'null') (only) removes the icon
1000
                if (typeof icon === 'string') {
1001
                    // set new icon
1002
                    currentIcon[id] = 'glyphicon-' + icon;
1003
                    $glyphIcon.addClass(currentIcon[id]);
1004
                }
1005
            }
1006
1007
            // show text
1008
            if (args !== null) {
1009
                // add jQuery object to it as first parameter
1010
                args.unshift($element);
1011
                // pass it to I18n
1012
                I18n._.apply(this, args);
1013
            }
1014
1015
            // show notification
1016
            $element.removeClass('hidden');
1017
        }
1018
1019
        /**
1020
         * display a status message
1021
         *
1022
         * This automatically passes the text to I18n for translation.
1023
         *
1024
         * @name   Alert.showStatus
1025
         * @function
1026
         * @param  {string|array} message     string, use an array for %s/%d options
1027
         * @param  {string|null}  icon        optional, the icon to show,
1028
         *                                    default: leave previous icon
1029
         * @param  {bool}         dismissable optional, whether the notification
1030
         *                                    can be dismissed (closed), default: false
1031
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1032
         *                                    notification should be hidden automatically;
1033
         *                                    default: disabled (0); use true for default value
1034
         */
1035
        me.showStatus = function(message, icon, dismissable, autoclose)
1036
        {
1037
            console.info('status shown: ', message);
1038
            // @TODO: implement dismissable
1039
            // @TODO: implement autoclose
1040
1041
            handleNotification(1, $statusMessage, message, icon);
1042
        }
1043
1044
        /**
1045
         * display an error message
1046
         *
1047
         * This automatically passes the text to I18n for translation.
1048
         *
1049
         * @name   Alert.showError
1050
         * @function
1051
         * @param  {string|array} message     string, use an array for %s/%d options
1052
         * @param  {string|null}  icon        optional, the icon to show, default:
1053
         *                                    leave previous icon
1054
         * @param  {bool}         dismissable optional, whether the notification
1055
         *                                    can be dismissed (closed), default: false
1056
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1057
         *                                    notification should be hidden automatically;
1058
         *                                    default: disabled (0); use true for default value
1059
         */
1060
        me.showError = function(message, icon, dismissable, autoclose)
1061
        {
1062
            console.error('error message shown: ', message);
1063
            // @TODO: implement dismissable (bootstrap add-on has it)
1064
            // @TODO: implement autoclose
1065
1066
            handleNotification(3, $errorMessage, message, icon);
1067
        }
1068
1069
        /**
1070
         * display remaining message
1071
         *
1072
         * This automatically passes the text to I18n for translation.
1073
         *
1074
         * @name   Alert.showRemaining
1075
         * @function
1076
         * @param  {string|array} message     string, use an array for %s/%d options
1077
         */
1078
        me.showRemaining = function(message)
1079
        {
1080
            console.info('remaining message shown: ', message);
1081
            handleNotification(1, $remainingTime, message);
1082
        }
1083
1084
        /**
1085
         * shows a loading message, optionally with a percentage
1086
         *
1087
         * This automatically passes all texts to the i10s module.
1088
         *
1089
         * @name   Alert.showLoading
1090
         * @function
1091
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1092
         * @param  {int}               percentage   optional, default: null
1093
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1094
         */
1095
        me.showLoading = function(message, percentage, icon)
1096
        {
1097
            if (typeof message !== 'undefined' && message !== null) {
1098
                console.info('status changed: ', message);
1099
            }
1100
1101
            // default message text
1102
            if (typeof message === 'undefined') {
1103
                message = 'Loading…';
1104
            }
1105
1106
            // currently percentage parameter is ignored
1107
            // // @TODO handle it here…
1108
1109
            handleNotification(0, $loadingIndicator, message, icon);
1110
1111
            // show loading status (cursor)
1112
            $('body').addClass('loading');
1113
        }
1114
1115
        /**
1116
         * hides the loading message
1117
         *
1118
         * @name   Alert.hideLoading
1119
         * @function
1120
         */
1121
        me.hideLoading = function()
1122
        {
1123
            $loadingIndicator.addClass('hidden');
1124
1125
            // hide loading cursor
1126
            $('body').removeClass('loading');
1127
        }
1128
1129
        /**
1130
         * hides any status/error messages
1131
         *
1132
         * This does not include the loading message.
1133
         *
1134
         * @name   Alert.hideMessages
1135
         * @function
1136
         */
1137
        me.hideMessages = function()
1138
        {
1139
            // also possible: $('.statusmessage').addClass('hidden');
1140
            $statusMessage.addClass('hidden');
1141
            $errorMessage.addClass('hidden');
1142
        }
1143
1144
        /**
1145
         * set a custom handler, which gets all notifications.
1146
         *
1147
         * This handler gets the following arguments:
1148
         * alertType (see array), $element, args, icon
1149
         * If it returns true, the own processing will be stopped so the message
1150
         * will not be displayed. Otherwise it will continue.
1151
         * As an aditional feature it can return q jQuery element, which will
1152
         * then be used to add the message there. Icons are not supported in
1153
         * that case and will be ignored.
1154
         * Pass 'null' to reset/delete the custom handler.
1155
         * Note that there is no notification when a message is supposed to get
1156
         * hidden.
1157
         *
1158
         * @name   Alert.setCustomHandler
1159
         * @function
1160
         * @param {function|null} newHandler
1161
         */
1162
        me.setCustomHandler = function(newHandler)
1163
        {
1164
            customHandler = newHandler;
1165
        }
1166
1167
        /**
1168
         * init status manager
1169
         *
1170
         * preloads jQuery elements
1171
         *
1172
         * @name   Alert.init
1173
         * @function
1174
         */
1175
        me.init = function()
1176
        {
1177
            // hide "no javascript" error message
1178
            $('#noscript').hide();
1179
1180
            // not a reset, but first set of the elements
1181
            $errorMessage = $('#errormessage');
1182
            $loadingIndicator = $('#loadingindicator');
1183
            $statusMessage = $('#status');
1184
            $remainingTime = $('#remainingtime');
1185
1186
            currentIcon = [
1187
                'glyphicon-time', // loading icon
1188
                'glyphicon-info-sign', // status icon
1189
                '', // reserved for warning, not used yet
1190
                'glyphicon-alert' // error icon
1191
            ];
1192
        }
1193
1194
        return me;
1195
    })();
1196
1197
    /**
1198
     * handles paste status/result
1199
     *
1200
     * @name   PasteStatus
1201
     * @class
1202
     */
1203
    var PasteStatus = (function () {
1204
        var me = {};
1205
1206
        var $pasteSuccess,
1207
            $pasteUrl,
1208
            $remainingTime,
1209
            $shortenButton;
1210
1211
        /**
1212
         * forward to URL shortener
1213
         *
1214
         * @name   PasteStatus.sendToShortener
1215
         * @private
1216
         * @function
1217
         * @param  {Event} event
1218
         */
1219
        function sendToShortener(event)
1220
        {
1221
            window.location.href = $shortenButton.data('shortener')
1222
                                   + encodeURIComponent($pasteUrl.attr('href'));
1223
        }
1224
1225
        /**
1226
         * Forces opening the paste if the link does not do this automatically.
1227
         *
1228
         * This is necessary as browsers will not reload the page when it is
1229
         * already loaded (which is fake as it is set via history.pushState()).
1230
         *
1231
         * @name   PasteStatus.pasteLinkClick
1232
         * @function
1233
         * @param  {Event} event
1234
         */
1235
        function pasteLinkClick(event)
1236
        {
1237
            // check if location is (already) shown in URL bar
1238
            if (window.location.href === $pasteUrl.attr('href')) {
1239
                // if so we need to load link by reloading the current site
1240
                window.location.reload(true);
1241
            }
1242
        }
1243
1244
        /**
1245
         * creates a notification after a successfull paste upload
1246
         *
1247
         * @name   PasteStatus.createPasteNotification
1248
         * @function
1249
         * @param  {string} url
1250
         * @param  {string} deleteUrl
1251
         */
1252
        me.createPasteNotification = function(url, deleteUrl)
1253
        {
1254
            $('#pastelink').html(
1255
                I18n._(
1256
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1257
                    url, url
1258
                )
1259
            );
1260
            // save newly created element
1261
            $pasteUrl = $('#pasteurl');
1262
            // and add click event
1263
            $pasteUrl.click(pasteLinkClick);
1264
1265
            // shorten button
1266
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1267
1268
            // show result
1269
            $pasteSuccess.removeClass('hidden');
1270
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1271
            Helper.selectText($pasteUrl[0]);
1272
        }
1273
1274
        /**
1275
         * shows the remaining time
1276
         *
1277
         * @name PasteStatus.showRemainingTime
1278
         * @function
1279
         * @param {object} pasteMetaData
1280
         */
1281
        me.showRemainingTime = function(pasteMetaData)
1282
        {
1283
            if (pasteMetaData.burnafterreading) {
1284
                // display paste "for your eyes only" if it is deleted
1285
1286
                // actually remove paste, before we claim it is deleted
1287
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1288
1289
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1290
                $remainingTime.addClass('foryoureyesonly');
1291
1292
                // discourage cloning (it cannot really be prevented)
1293
                TopNav.hideCloneButton();
1294
1295
            } else if (pasteMetaData.expire_date) {
1296
                // display paste expiration
1297
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1298
                    expirationLabel = [
1299
                        'This document will expire in %d ' + expiration[1] + '.',
1300
                        'This document will expire in %d ' + expiration[1] + 's.'
1301
                    ];
1302
1303
                Alert.showRemaining([expirationLabel, expiration[0]]);
1304
                $remainingTime.removeClass('foryoureyesonly');
1305
            } else {
1306
                // never expires
1307
                return;
1308
            }
1309
1310
            // in the end, display notification
1311
            $remainingTime.removeClass('hidden');
1312
        }
1313
1314
        /**
1315
         * hides the remaining time and successful upload notification
1316
         *
1317
         * @name PasteStatus.hideRemainingTime
1318
         * @function
1319
         */
1320
        me.hideMessages = function()
1321
        {
1322
            $remainingTime.addClass('hidden');
1323
            $pasteSuccess.addClass('hidden');
1324
        }
1325
1326
        /**
1327
         * init status manager
1328
         *
1329
         * preloads jQuery elements
1330
         *
1331
         * @name   PasteStatus.init
1332
         * @function
1333
         */
1334
        me.init = function()
1335
        {
1336
            $pasteSuccess = $('#pastesuccess');
1337
            // $pasteUrl is saved in me.createPasteNotification() after creation
1338
            $remainingTime = $('#remainingtime');
1339
            $shortenButton = $('#shortenbutton');
1340
1341
            // bind elements
1342
            $shortenButton.click(sendToShortener);
1343
        }
1344
1345
        return me;
1346
    })();
1347
1348
    /**
1349
     * password prompt
1350
     *
1351
     * @name Prompt
1352
     * @class
1353
     */
1354
    var Prompt = (function () {
1355
        var me = {};
1356
1357
        var $passwordDecrypt,
1358
            $passwordForm,
1359
            $passwordModal;
1360
1361
        var password = '';
1362
1363
        /**
1364
         * submit a password in the modal dialog
1365
         *
1366
         * @name Prompt.submitPasswordModal
1367
         * @private
1368
         * @function
1369
         * @param  {Event} event
1370
         */
1371
        function submitPasswordModal(event)
1372
        {
1373
            event.preventDefault();
1374
1375
            // get input
1376
            password = $passwordDecrypt.val();
1377
1378
            // hide modal
1379
            $passwordModal.modal('hide');
1380
1381
            PasteDecrypter.run();
1382
        }
1383
1384
        /**
1385
         * ask the user for the password and set it
1386
         *
1387
         * @name Prompt.requestPassword
1388
         * @function
1389
         */
1390
        me.requestPassword = function()
1391
        {
1392
            // show new bootstrap method (if available)
1393
            if ($passwordModal.length !== 0) {
1394
                $passwordModal.modal({
1395
                    backdrop: 'static',
1396
                    keyboard: false
1397
                });
1398
                return;
1399
            }
1400
1401
            // fallback to old method for page template
1402
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1403
            if (newPassword === null) {
1404
                throw 'password prompt canceled';
1405
            }
1406
            if (password.length === 0) {
1407
                // recurse…
1408
                return me.requestPassword();
1409
            }
1410
1411
            password = newPassword;
1412
        }
1413
1414
        /**
1415
         * get the cached password
1416
         *
1417
         * If you do not get a password with this function
1418
         * (returns an empty string), use requestPassword.
1419
         *
1420
         * @name   Prompt.getPassword
1421
         * @function
1422
         * @return {string}
1423
         */
1424
        me.getPassword = function()
1425
        {
1426
            return password;
1427
        }
1428
1429
        /**
1430
         * init status manager
1431
         *
1432
         * preloads jQuery elements
1433
         *
1434
         * @name   Prompt.init
1435
         * @function
1436
         */
1437
        me.init = function()
1438
        {
1439
            $passwordDecrypt = $('#passworddecrypt');
1440
            $passwordForm = $('#passwordform');
1441
            $passwordModal = $('#passwordmodal');
1442
1443
            // bind events
1444
1445
            // focus password input when it is shown
1446
            $passwordModal.on('shown.bs.Model', function () {
1447
                $passwordDecrypt.focus();
1448
            });
1449
            // handle Model password submission
1450
            $passwordForm.submit(submitPasswordModal);
1451
        }
1452
1453
        return me;
1454
    })();
1455
1456
    /**
1457
     * Manage paste/message input, and preview tab
1458
     *
1459
     * Note that the actual preview is handled by PasteViewer.
1460
     *
1461
     * @name   Editor
1462
     * @class
1463
     */
1464
    var Editor = (function () {
1465
        var me = {};
1466
1467
        var $editorTabs,
1468
            $messageEdit,
1469
            $messagePreview,
1470
            $message;
1471
1472
        var isPreview = false;
1473
1474
        /**
1475
         * support input of tab character
1476
         *
1477
         * @name   Editor.supportTabs
1478
         * @function
1479
         * @param  {Event} event
1480
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1481
         */
1482
        function supportTabs(event)
1483
        {
1484
            var keyCode = event.keyCode || event.which;
1485
            // tab was pressed
1486
            if (keyCode === 9) {
1487
                // get caret position & selection
1488
                var val   = this.value,
1489
                    start = this.selectionStart,
1490
                    end   = this.selectionEnd;
1491
                // set textarea value to: text before caret + tab + text after caret
1492
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1493
                // put caret at right position again
1494
                this.selectionStart = this.selectionEnd = start + 1;
1495
                // prevent the textarea to lose focus
1496
                event.preventDefault();
1497
            }
1498
        }
1499
1500
        /**
1501
         * view the Editor tab
1502
         *
1503
         * @name   Editor.viewEditor
1504
         * @function
1505
         * @param  {Event} event - optional
1506
         */
1507
        function viewEditor(event)
1508
        {
1509
            // toggle buttons
1510
            $messageEdit.addClass('active');
1511
            $messagePreview.removeClass('active');
1512
1513
            PasteViewer.hide();
1514
1515
            // reshow input
1516
            $message.removeClass('hidden');
1517
1518
            me.focusInput();
1519
1520
            // finish
1521
            isPreview = false;
1522
1523
            // prevent jumping of page to top
1524
            if (typeof event !== 'undefined') {
1525
                event.preventDefault();
1526
            }
1527
        }
1528
1529
        /**
1530
         * view the preview tab
1531
         *
1532
         * @name   Editor.viewPreview
1533
         * @function
1534
         * @param  {Event} event
1535
         */
1536
        function viewPreview(event)
1537
        {
1538
            // toggle buttons
1539
            $messageEdit.removeClass('active');
1540
            $messagePreview.addClass('active');
1541
1542
            // hide input as now preview is shown
1543
            $message.addClass('hidden');
1544
1545
            // show preview
1546
            PasteViewer.setText($message.val());
1547
            PasteViewer.run();
1548
1549
            // finish
1550
            isPreview = true;
1551
1552
            // prevent jumping of page to top
1553
            if (typeof event !== 'undefined') {
1554
                event.preventDefault();
1555
            }
1556
        }
1557
1558
        /**
1559
         * get the state of the preview
1560
         *
1561
         * @name   Editor.isPreview
1562
         * @function
1563
         */
1564
        me.isPreview = function()
1565
        {
1566
            return isPreview;
1567
        }
1568
1569
        /**
1570
         * reset the Editor view
1571
         *
1572
         * @name   Editor.resetInput
1573
         * @function
1574
         */
1575
        me.resetInput = function()
1576
        {
1577
            // go back to input
1578
            if (isPreview) {
1579
                viewEditor();
1580
            }
1581
1582
            // clear content
1583
            $message.val('');
1584
        }
1585
1586
        /**
1587
         * shows the Editor
1588
         *
1589
         * @name   Editor.show
1590
         * @function
1591
         */
1592
        me.show = function()
1593
        {
1594
            $message.removeClass('hidden');
1595
            $editorTabs.removeClass('hidden');
1596
        }
1597
1598
        /**
1599
         * hides the Editor
1600
         *
1601
         * @name   Editor.reset
1602
         * @function
1603
         */
1604
        me.hide = function()
1605
        {
1606
            $message.addClass('hidden');
1607
            $editorTabs.addClass('hidden');
1608
        }
1609
1610
        /**
1611
         * focuses the message input
1612
         *
1613
         * @name   Editor.focusInput
1614
         * @function
1615
         */
1616
        me.focusInput = function()
1617
        {
1618
            $message.focus();
1619
        }
1620
1621
        /**
1622
         * sets a new text
1623
         *
1624
         * @name   Editor.setText
1625
         * @function
1626
         * @param {string} newText
1627
         */
1628
        me.setText = function(newText)
1629
        {
1630
            $message.val(newText);
1631
        }
1632
1633
        /**
1634
         * returns the current text
1635
         *
1636
         * @name   Editor.getText
1637
         * @function
1638
         * @return {string}
1639
         */
1640
        me.getText = function()
1641
        {
1642
            return $message.val()
1643
        }
1644
1645
        /**
1646
         * init status manager
1647
         *
1648
         * preloads jQuery elements
1649
         *
1650
         * @name   Editor.init
1651
         * @function
1652
         */
1653
        me.init = function()
1654
        {
1655
            $editorTabs = $('#editorTabs');
1656
            $message = $('#message');
1657
1658
            // bind events
1659
            $message.keydown(supportTabs);
1660
1661
            // bind click events to tab switchers (a), but save parent of them
1662
            // (li)
1663
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1664
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1665
        }
1666
1667
        return me;
1668
    })();
1669
1670
    /**
1671
     * (view) Parse and show paste.
1672
     *
1673
     * @name   PasteViewer
1674
     * @class
1675
     */
1676
    var PasteViewer = (function () {
1677
        var me = {};
1678
1679
        var $placeholder,
1680
            $prettyMessage,
1681
            $prettyPrint,
1682
            $plainText;
1683
1684
        var text,
1685
            format = 'plaintext',
1686
            isDisplayed = false,
1687
            isChanged = true; // by default true as nothing was parsed yet
1688
1689
        /**
1690
         * apply the set format on paste and displays it
1691
         *
1692
         * @name   PasteViewer.parsePaste
1693
         * @private
1694
         * @function
1695
         */
1696
        function parsePaste()
1697
        {
1698
            // skip parsing if no text is given
1699
            if (text === '') {
1700
                return;
1701
            }
1702
1703
            // set sanitized and linked text
1704
            var sanitizedLinkedText = DOMPurify.sanitize(Helper.urls2links(text));
1705
            $plainText.html(sanitizedLinkedText);
1706
            $prettyPrint.html(sanitizedLinkedText);
1707
1708
            switch (format) {
1709
                case 'markdown':
1710
                    var converter = new showdown.Converter({
1711
                        strikethrough: true,
1712
                        tables: true,
1713
                        tablesHeaderId: true
1714
                    });
1715
                    // let showdown convert the HTML and sanitize HTML *afterwards*!
1716
                    $plainText.html(
1717
                        DOMPurify.sanitize(converter.makeHtml(text))
1718
                    );
1719
                    // add table classes from bootstrap css
1720
                    $plainText.find('table').addClass('table-condensed table-bordered');
1721
                    break;
1722
                case 'syntaxhighlighting':
1723
                    // yes, this is really needed to initialize the environment
1724
                    if (typeof prettyPrint === 'function')
1725
                    {
1726
                        prettyPrint();
1727
                    }
1728
1729
                    $prettyPrint.html(
1730
                        DOMPurify.sanitize(
1731
                            prettyPrintOne(Helper.urls2links(text), null, true)
1732
                        )
1733
                    );
1734
                    // fall through, as the rest is the same
1735
                default: // = 'plaintext'
1736
                    $prettyPrint.css('white-space', 'pre-wrap');
1737
                    $prettyPrint.css('word-break', 'normal');
1738
                    $prettyPrint.removeClass('prettyprint');
1739
            }
1740
        }
1741
1742
        /**
1743
         * displays the paste
1744
         *
1745
         * @name   PasteViewer.showPaste
1746
         * @private
1747
         * @function
1748
         */
1749
        function showPaste()
1750
        {
1751
            // instead of "nothing" better display a placeholder
1752
            if (text === '') {
1753
                $placeholder.removeClass('hidden')
1754
                return;
1755
            }
1756
            // otherwise hide the placeholder
1757
            $placeholder.addClass('hidden')
1758
1759
            switch (format) {
1760
                case 'markdown':
1761
                    $plainText.removeClass('hidden');
1762
                    $prettyMessage.addClass('hidden');
1763
                    break;
1764
                default:
1765
                    $plainText.addClass('hidden');
1766
                    $prettyMessage.removeClass('hidden');
1767
                    break;
1768
            }
1769
        }
1770
1771
        /**
1772
         * sets the format in which the text is shown
1773
         *
1774
         * @name   PasteViewer.setFormat
1775
         * @function
1776
         * @param {string} newFormat the new format
1777
         */
1778
        me.setFormat = function(newFormat)
1779
        {
1780
            // skip if there is no update
1781
            if (format === newFormat) {
1782
                return;
1783
            }
1784
1785
            // needs to update display too, if we switch from or to Markdown
1786
            if (format === 'markdown' || newFormat === 'markdown') {
1787
                isDisplayed = false;
1788
            }
1789
1790
            format = newFormat;
1791
            isChanged = true;
1792
        }
1793
1794
        /**
1795
         * returns the current format
1796
         *
1797
         * @name   PasteViewer.getFormat
1798
         * @function
1799
         * @return {string}
1800
         */
1801
        me.getFormat = function()
1802
        {
1803
            return format;
1804
        }
1805
1806
        /**
1807
         * returns whether the current view is pretty printed
1808
         *
1809
         * @name   PasteViewer.isPrettyPrinted
1810
         * @function
1811
         * @return {bool}
1812
         */
1813
        me.isPrettyPrinted = function()
1814
        {
1815
            return $prettyPrint.hasClass('prettyprinted');
1816
        }
1817
1818
        /**
1819
         * sets the text to show
1820
         *
1821
         * @name   PasteViewer.setText
1822
         * @function
1823
         * @param {string} newText the text to show
1824
         */
1825
        me.setText = function(newText)
1826
        {
1827
            // escape HTML entities
1828
            newText = $('<div />').text(newText).html();
1829
            if (text !== newText) {
1830
                text = newText;
1831
                isChanged = true;
1832
            }
1833
        }
1834
1835
        /**
1836
         * gets the current cached text
1837
         *
1838
         * @name   PasteViewer.getText
1839
         * @function
1840
         * @return {string}
1841
         */
1842
        me.getText = function()
1843
        {
1844
            return text;
1845
        }
1846
1847
        /**
1848
         * show/update the parsed text (preview)
1849
         *
1850
         * @name   PasteViewer.run
1851
         * @function
1852
         */
1853
        me.run = function()
1854
        {
1855
            if (isChanged) {
1856
                parsePaste();
1857
                isChanged = false;
1858
            }
1859
1860
            if (!isDisplayed) {
1861
                showPaste();
1862
                isDisplayed = true;
1863
            }
1864
        }
1865
1866
        /**
1867
         * hide parsed text (preview)
1868
         *
1869
         * @name   PasteViewer.hide
1870
         * @function
1871
         */
1872
        me.hide = function()
1873
        {
1874
            if (!isDisplayed) {
1875
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1876
            }
1877
1878
            $plainText.addClass('hidden');
1879
            $prettyMessage.addClass('hidden');
1880
            $placeholder.addClass('hidden');
1881
1882
            isDisplayed = false;
1883
        }
1884
1885
        /**
1886
         * init status manager
1887
         *
1888
         * preloads jQuery elements
1889
         *
1890
         * @name   PasteViewer.init
1891
         * @function
1892
         */
1893
        me.init = function()
1894
        {
1895
            $placeholder = $('#placeholder');
1896
            $plainText = $('#plaintext');
1897
            $prettyMessage = $('#prettymessage');
1898
            $prettyPrint = $('#prettyprint');
1899
1900
            // check requirements
1901
            if (typeof prettyPrintOne !== 'function') {
1902
                Alert.showError([
1903
                    'The library %s is not available. This may cause display errors.',
1904
                    'pretty print'
1905
                ]);
1906
            }
1907
            if (typeof showdown !== 'object') {
1908
                Alert.showError([
1909
                    'The library %s is not available. This may cause display errors.',
1910
                    'showdown'
1911
                ]);
1912
            }
1913
1914
            // get default option from template/HTML or fall back to set value
1915
            format = Model.getFormatDefault() || format;
1916
            text = '';
1917
            isDisplayed = false;
1918
            isChanged = true;
1919
        }
1920
1921
        return me;
1922
    })();
1923
1924
    /**
1925
     * (view) Show attachment and preview if possible
1926
     *
1927
     * @name   AttachmentViewer
1928
     * @param  {object} window
1929
     * @param  {object} document
1930
     * @class
1931
     */
1932
    var AttachmentViewer = (function (window, document) {
1933
        var me = {};
1934
1935
        var $attachmentLink,
1936
            $attachmentPreview,
1937
            $attachment;
1938
1939
        var attachmentHasPreview = false;
1940
1941
        /**
1942
         * sets the attachment but does not yet show it
1943
         *
1944
         * @name   AttachmentViewer.setAttachment
1945
         * @function
1946
         * @param {string} attachmentData - base64-encoded data of file
1947
         * @param {string} fileName - optional, file name
1948
         */
1949
        me.setAttachment = function(attachmentData, fileName)
1950
        {
1951
            var imagePrefix = 'data:image/';
1952
1953
            $attachmentLink.attr('href', attachmentData);
1954
            if (typeof fileName !== 'undefined') {
1955
                $attachmentLink.attr('download', fileName);
1956
            }
1957
1958
            // if the attachment is an image, display it
1959
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
1960
                $attachmentPreview.html(
1961
                    $(document.createElement('img'))
1962
                        .attr('src', attachmentData)
1963
                        .attr('class', 'img-thumbnail')
1964
                );
1965
                attachmentHasPreview = true;
1966
            }
1967
        }
1968
1969
        /**
1970
         * displays the attachment
1971
         *
1972
         * @name AttachmentViewer.showAttachment
1973
         * @function
1974
         */
1975
        me.showAttachment = function()
1976
        {
1977
            $attachment.removeClass('hidden');
1978
1979
            if (attachmentHasPreview) {
1980
                $attachmentPreview.removeClass('hidden');
1981
            }
1982
        }
1983
1984
        /**
1985
         * removes the attachment
1986
         *
1987
         * This automatically hides the attachment containers to, to
1988
         * prevent an inconsistent display.
1989
         *
1990
         * @name AttachmentViewer.removeAttachment
1991
         * @function
1992
         */
1993
        me.removeAttachment = function()
1994
        {
1995
            me.hideAttachment();
1996
            me.hideAttachmentPreview();
1997
            $attachmentLink.prop('href', '');
1998
            $attachmentLink.prop('download', '');
1999
            $attachmentPreview.html('');
2000
        }
2001
2002
        /**
2003
         * hides the attachment
2004
         *
2005
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2006
         * for that) nor will it hide the attachment link if it was moved somewhere
2007
         * else (see AttachmentViewer.moveAttachmentTo).
2008
         *
2009
         * @name AttachmentViewer.hideAttachment
2010
         * @function
2011
         */
2012
        me.hideAttachment = function()
2013
        {
2014
            $attachment.addClass('hidden');
2015
        }
2016
2017
        /**
2018
         * hides the attachment preview
2019
         *
2020
         * @name AttachmentViewer.hideAttachmentPreview
2021
         * @function
2022
         */
2023
        me.hideAttachmentPreview = function()
2024
        {
2025
            $attachmentPreview.addClass('hidden');
2026
        }
2027
2028
        /**
2029
         * checks if there is an attachment
2030
         *
2031
         * @name   AttachmentViewer.hasAttachment
2032
         * @function
2033
         */
2034
        me.hasAttachment = function()
2035
        {
2036
            var link = $attachmentLink.prop('href');
2037
            return (typeof link !== 'undefined' && link !== '')
2038
        }
2039
2040
        /**
2041
         * return the attachment
2042
         *
2043
         * @name   AttachmentViewer.getAttachment
2044
         * @function
2045
         * @returns {array}
2046
         */
2047
        me.getAttachment = function()
2048
        {
2049
            return [
2050
                $attachmentLink.prop('href'),
2051
                $attachmentLink.prop('download')
2052
            ];
2053
        }
2054
2055
        /**
2056
         * moves the attachment link to another element
2057
         *
2058
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2059
         *
2060
         * @name   AttachmentViewer.moveAttachmentTo
2061
         * @function
2062
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2063
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2064
         */
2065
        me.moveAttachmentTo = function($element, label)
2066
        {
2067
            // move elemement to new place
2068
            $attachmentLink.appendTo($element);
2069
2070
            // update text
2071
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2072
        }
2073
2074
        /**
2075
         * initiate
2076
         *
2077
         * preloads jQuery elements
2078
         *
2079
         * @name   AttachmentViewer.init
2080
         * @function
2081
         */
2082
        me.init = function()
2083
        {
2084
            $attachment = $('#attachment');
2085
            $attachmentLink = $('#attachment a');
2086
            $attachmentPreview = $('#attachmentPreview');
2087
        }
2088
2089
        return me;
2090
    })(window, document);
2091
2092
    /**
2093
     * (view) Shows discussion thread and handles replies
2094
     *
2095
     * @name   DiscussionViewer
2096
     * @param  {object} window
2097
     * @param  {object} document
2098
     * @class
2099
     */
2100
    var DiscussionViewer = (function (window, document) {
2101
        var me = {};
2102
2103
        var $commentTail,
2104
            $discussion,
2105
            $reply,
2106
            $replyMessage,
2107
            $replyNickname,
2108
            $replyStatus,
2109
            $commentContainer;
2110
2111
        var replyCommentId;
2112
2113
        /**
2114
         * initializes the templates
2115
         *
2116
         * @name   DiscussionViewer.initTemplates
2117
         * @private
2118
         * @function
2119
         */
2120
        function initTemplates()
2121
        {
2122
            $reply = Model.getTemplate('reply');
2123
            $replyMessage = $reply.find('#replymessage');
2124
            $replyNickname = $reply.find('#nickname');
2125
            $replyStatus = $reply.find('#replystatus');
2126
2127
            // cache jQuery elements
2128
            $commentTail = Model.getTemplate('commenttail');
2129
        }
2130
2131
        /**
2132
         * open the comment entry when clicking the "Reply" button of a comment
2133
         *
2134
         * @name   DiscussionViewer.openReply
2135
         * @private
2136
         * @function
2137
         * @param  {Event} event
2138
         */
2139
        function openReply(event)
2140
        {
2141
            var $source = $(event.target);
2142
2143
            // clear input
2144
            $replyMessage.val('');
2145
            $replyNickname.val('');
2146
2147
            // get comment id from source element
2148
            replyCommentId = $source.parent().prop('id').split('_')[1];
2149
2150
            // move to correct position
2151
            $source.after($reply);
2152
2153
            // show
2154
            $reply.removeClass('hidden');
2155
            $replyMessage.focus();
2156
2157
            event.preventDefault();
2158
        }
2159
2160
        /**
2161
         * custom handler for displaying notifications in own status message area
2162
         *
2163
         * @name   DiscussionViewer.handleNotification
2164
         * @function
2165
         * @param  {string} alertType
2166
         * @param  {jQuery} $element
2167
         * @param  {string|array} args
2168
         * @param  {string|null} icon
2169
         * @return {bool|jQuery}
2170
         */
2171
        me.handleNotification = function(alertType, $element, args, icon)
2172
        {
2173
            // ignore loading messages
2174
            if (alertType === 'loading') {
2175
                return false;
2176
            }
2177
2178
            if (alertType === 'danger') {
2179
                $replyStatus.removeClass('alert-info');
2180
                $replyStatus.addClass('alert-danger');
2181
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2182
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2183
            } else {
2184
                $replyStatus.removeClass('alert-danger');
2185
                $replyStatus.addClass('alert-info');
2186
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2187
                $replyStatus.find(':first').addClass('glyphicon-alert');
2188
            }
2189
2190
            return $replyStatus;
2191
        }
2192
2193
        /**
2194
         * adds another comment
2195
         *
2196
         * @name   DiscussionViewer.addComment
2197
         * @function
2198
         * @param {object} comment
2199
         * @param {string} commentText
2200
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2201
         */
2202
        me.addComment = function(comment, commentText, nickname, $place)
2203
        {
2204
            if (typeof $place === 'undefined') {
2205
                // starting point (default value/fallback)
2206
                $place = $commentContainer;
2207
2208
                // if parent comment exists
2209
                var $parentComment = $('#comment_' + comment.parentid);
2210
                if ($parentComment.length) {
2211
                    // use parent as position for noew comment, so it shifted
2212
                    // to the right
2213
                    $place = $parentComment;
2214
                }
2215
            }
2216
            if (commentText === '') {
2217
                commentText = 'comment decryption failed';
2218
            }
2219
2220
            // create new comment based on template
2221
            var $commentEntry = Model.getTemplate('comment');
2222
            $commentEntry.prop('id', 'comment_' + comment.id);
2223
            var $commentEntryData = $commentEntry.find('div.commentdata');
2224
2225
            // set & parse text
2226
            $commentEntryData.html(
2227
                DOMPurify.sanitize(
2228
                    Helper.urls2links(commentText)
2229
                )
2230
            );
2231
2232
            // set nickname
2233
            if (nickname.length > 0) {
2234
                $commentEntry.find('span.nickname').text(nickname);
2235
            } else {
2236
                $commentEntry.find('span.nickname').html('<i></i>');
2237
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2238
            }
2239
2240
            // set date
2241
            $commentEntry.find('span.commentdate')
2242
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2243
                      .attr('title', 'CommentID: ' + comment.id);
2244
2245
            // if an avatar is available, display it
2246
            if (comment.meta.vizhash) {
2247
                $commentEntry.find('span.nickname')
2248
                             .before(
2249
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2250
                             );
2251
                $(document).on('languageLoaded', function () {
2252
                    $commentEntry.find('img.vizhash')
2253
                                 .prop('title', I18n._('Avatar generated from IP address'));
2254
                });
2255
            }
2256
2257
            // finally append comment
2258
            $place.append($commentEntry);
2259
        }
2260
2261
        /**
2262
         * finishes the discussion area after last comment
2263
         *
2264
         * @name   DiscussionViewer.finishDiscussion
2265
         * @function
2266
         */
2267
        me.finishDiscussion = function()
2268
        {
2269
            // add 'add new comment' area
2270
            $commentContainer.append($commentTail);
2271
2272
            // show discussions
2273
            $discussion.removeClass('hidden');
2274
        }
2275
2276
        /**
2277
         * shows the discussion area
2278
         *
2279
         * @name   DiscussionViewer.showDiscussion
2280
         * @function
2281
         */
2282
        me.showDiscussion = function()
2283
        {
2284
            $discussion.removeClass('hidden');
2285
        }
2286
2287
        /**
2288
         * removes the old discussion and prepares everything for creating a new
2289
         * one.
2290
         *
2291
         * @name   DiscussionViewer.prepareNewDisucssion
2292
         * @function
2293
         */
2294
        me.prepareNewDisucssion = function()
2295
        {
2296
            $commentContainer.html('');
2297
            $discussion.addClass('hidden');
2298
2299
            // (re-)init templates
2300
            initTemplates();
2301
        }
2302
2303
        /**
2304
         * returns the user put into the reply form
2305
         *
2306
         * @name   DiscussionViewer.getReplyData
2307
         * @function
2308
         * @return {array}
2309
         */
2310
        me.getReplyData = function()
2311
        {
2312
            return [
2313
                $replyMessage.val(),
2314
                $replyNickname.val()
2315
            ];
2316
        }
2317
2318
        /**
2319
         * highlights a specific comment and scrolls to it if necessary
2320
         *
2321
         * @name   DiscussionViewer.highlightComment
2322
         * @function
2323
         * @param {string} commentId
2324
         * @param {bool} fadeOut - whether to fade out the comment
2325
         */
2326
        me.highlightComment = function(commentId, fadeOut)
2327
        {
2328
            var $comment = $('#comment_' + commentId);
2329
            // in case comment does not exist, cancel
2330
            if ($comment.length === 0) {
2331
                return;
2332
            }
2333
2334
            var highlightComment = function () {
2335
                $comment.addClass('highlight');
2336
                if (fadeOut === true) {
2337
                    setTimeout(function () {
2338
                        $comment.removeClass('highlight');
2339
                    }, 300);
2340
                }
2341
            }
2342
2343
            if (UiHelper.isVisible($comment)) {
2344
                return highlightComment();
2345
            }
2346
2347
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2348
        }
2349
2350
        /**
2351
         * returns the id of the parent comment the user is replying to
2352
         *
2353
         * @name   DiscussionViewer.getReplyCommentId
2354
         * @function
2355
         * @return {int|undefined}
2356
         */
2357
        me.getReplyCommentId = function()
2358
        {
2359
            return replyCommentId;
2360
        }
2361
2362
        /**
2363
         * initiate
2364
         *
2365
         * preloads jQuery elements
2366
         *
2367
         * @name   DiscussionViewer.init
2368
         * @function
2369
         */
2370
        me.init = function()
2371
        {
2372
            // bind events to templates (so they are later cloned)
2373
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2374
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2375
2376
            $commentContainer = $('#commentcontainer');
2377
            $discussion = $('#discussion');
2378
        }
2379
2380
        return me;
2381
    })(window, document);
2382
2383
    /**
2384
     * Manage top (navigation) bar
2385
     *
2386
     * @name   TopNav
2387
     * @param  {object} window
2388
     * @param  {object} document
2389
     * @class
2390
     */
2391
    var TopNav = (function (window, document) {
2392
        var me = {};
2393
2394
        var createButtonsDisplayed = false;
2395
        var viewButtonsDisplayed = false;
2396
2397
        var $attach,
2398
            $burnAfterReading,
2399
            $burnAfterReadingOption,
2400
            $cloneButton,
2401
            $customAttachment,
2402
            $expiration,
2403
            $fileRemoveButton,
2404
            $fileWrap,
2405
            $formatter,
2406
            $newButton,
2407
            $openDiscussion,
2408
            $openDiscussionOption,
2409
            $password,
2410
            $passwordInput,
2411
            $rawTextButton,
2412
            $qrCodeLink,
2413
            $sendButton;
2414
2415
        var pasteExpiration = '1week';
2416
2417
        /**
2418
         * set the expiration on bootstrap templates in dropdown
2419
         *
2420
         * @name   TopNav.updateExpiration
2421
         * @private
2422
         * @function
2423
         * @param  {Event} event
2424
         */
2425
        function updateExpiration(event)
2426
        {
2427
            // get selected option
2428
            var target = $(event.target);
2429
2430
            // update dropdown display and save new expiration time
2431
            $('#pasteExpirationDisplay').text(target.text());
2432
            pasteExpiration = target.data('expiration');
2433
2434
            event.preventDefault();
2435
        }
2436
2437
        /**
2438
         * set the format on bootstrap templates in dropdown
2439
         *
2440
         * @name   TopNav.updateFormat
2441
         * @private
2442
         * @function
2443
         * @param  {Event} event
2444
         */
2445
        function updateFormat(event)
2446
        {
2447
            // get selected option
2448
            var $target = $(event.target);
2449
2450
            // update dropdown display and save new format
2451
            var newFormat = $target.data('format');
2452
            $('#pasteFormatterDisplay').text($target.text());
2453
            PasteViewer.setFormat(newFormat);
2454
2455
            // update preview
2456
            if (Editor.isPreview()) {
2457
                PasteViewer.run();
2458
            }
2459
2460
            event.preventDefault();
2461
        }
2462
2463
        /**
2464
         * when "burn after reading" is checked, disable discussion
2465
         *
2466
         * @name   TopNav.changeBurnAfterReading
2467
         * @private
2468
         * @function
2469
         */
2470
        function changeBurnAfterReading()
2471
        {
2472
            if ($burnAfterReading.is(':checked')) {
2473
                $openDiscussionOption.addClass('buttondisabled');
2474
                $openDiscussion.prop('checked', false);
2475
2476
                // if button is actually disabled, force-enable it and uncheck other button
2477
                $burnAfterReadingOption.removeClass('buttondisabled');
2478
            } else {
2479
                $openDiscussionOption.removeClass('buttondisabled');
2480
            }
2481
        }
2482
2483
        /**
2484
         * when discussion is checked, disable "burn after reading"
2485
         *
2486
         * @name   TopNav.changeOpenDiscussion
2487
         * @private
2488
         * @function
2489
         */
2490
        function changeOpenDiscussion()
2491
        {
2492
            if ($openDiscussion.is(':checked')) {
2493
                $burnAfterReadingOption.addClass('buttondisabled');
2494
                $burnAfterReading.prop('checked', false);
2495
2496
                // if button is actually disabled, force-enable it and uncheck other button
2497
                $openDiscussionOption.removeClass('buttondisabled');
2498
            } else {
2499
                $burnAfterReadingOption.removeClass('buttondisabled');
2500
            }
2501
        }
2502
2503
        /**
2504
         * return raw text
2505
         *
2506
         * @name   TopNav.rawText
2507
         * @private
2508
         * @function
2509
         * @param  {Event} event
2510
         */
2511
        function rawText(event)
2512
        {
2513
            TopNav.hideAllButtons();
2514
            Alert.showLoading('Showing raw text…', 0, 'time');
2515
            var paste = PasteViewer.getText();
2516
2517
            // push a new state to allow back navigation with browser back button
2518
            history.pushState(
2519
                {type: 'raw'},
2520
                document.title,
2521
                // recreate paste URL
2522
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2523
                Model.getPasteKey()
2524
            );
2525
2526
            // we use text/html instead of text/plain to avoid a bug when
2527
            // reloading the raw text view (it reverts to type text/html)
2528
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2529
            var newDoc = document.open('text/html', 'replace');
2530
            newDoc.write('<!DOCTYPE html><html><head>');
2531
            for (var i = 0; i < $head.length; i++) {
2532
                newDoc.write($head[i].outerHTML);
2533
            }
2534
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2535
            newDoc.close();
2536
        }
2537
2538
        /**
2539
         * saves the language in a cookie and reloads the page
2540
         *
2541
         * @name   TopNav.setLanguage
2542
         * @private
2543
         * @function
2544
         * @param  {Event} event
2545
         */
2546
        function setLanguage(event)
2547
        {
2548
            document.cookie = 'lang=' + $(event.target).data('lang');
2549
            UiHelper.reloadHome();
2550
        }
2551
2552
        /**
2553
         * hides all messages and creates a new paste
2554
         *
2555
         * @name   TopNav.clickNewPaste
2556
         * @private
2557
         * @function
2558
         * @param  {Event} event
2559
         */
2560
        function clickNewPaste(event)
2561
        {
2562
            Controller.hideStatusMessages();
2563
            Controller.newPaste();
2564
        }
2565
2566
        /**
2567
         * removes the existing attachment
2568
         *
2569
         * @name   TopNav.removeAttachment
2570
         * @private
2571
         * @function
2572
         * @param  {Event} event
2573
         */
2574
        function removeAttachment(event)
2575
        {
2576
            // if custom attachment is used, remove it first
2577
            if (!$customAttachment.hasClass('hidden')) {
2578
                AttachmentViewer.removeAttachment();
2579
                $customAttachment.addClass('hidden');
2580
                $fileWrap.removeClass('hidden');
2581
            }
2582
2583
            // our up-to-date jQuery can handle it :)
2584
            $fileWrap.find('input').val('');
2585
2586
            // pevent '#' from appearing in the URL
2587
            event.preventDefault();
2588
        }
2589
2590
        /**
2591
         * Shows the QR code of the current paste (URL).
2592
         *
2593
         * @name   TopNav.displayQrCode
2594
         * @function
2595
         * @param  {Event} event
2596
         */
2597
        function displayQrCode(event)
0 ignored issues
show
The parameter event is not used and could be removed.

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

Loading history...
2598
        {
2599
            var qrCanvas = kjua({
2600
                render: 'canvas',
2601
                text: window.location.href
2602
            });
2603
            $('#qrcode-display').html(qrCanvas);
2604
        }
2605
2606
        /**
2607
         * Shows all elements belonging to viwing an existing pastes
2608
         *
2609
         * @name   TopNav.showViewButtons
2610
         * @function
2611
         */
2612
        me.showViewButtons = function()
2613
        {
2614
            if (viewButtonsDisplayed) {
2615
                console.warn('showViewButtons: view buttons are already displayed');
2616
                return;
2617
            }
2618
2619
            $newButton.removeClass('hidden');
2620
            $cloneButton.removeClass('hidden');
2621
            $rawTextButton.removeClass('hidden');
2622
            $qrCodeLink.removeClass('hidden');
2623
2624
            viewButtonsDisplayed = true;
2625
        }
2626
2627
        /**
2628
         * Hides all elements belonging to existing pastes
2629
         *
2630
         * @name   TopNav.hideViewButtons
2631
         * @function
2632
         */
2633
        me.hideViewButtons = function()
2634
        {
2635
            if (!viewButtonsDisplayed) {
2636
                console.warn('hideViewButtons: view buttons are already hidden');
2637
                return;
2638
            }
2639
2640
            $newButton.addClass('hidden');
2641
            $cloneButton.addClass('hidden');
2642
            $rawTextButton.addClass('hidden');
2643
            $qrCodeLink.addClass('hidden');
2644
2645
            viewButtonsDisplayed = false;
2646
        }
2647
2648
        /**
2649
         * Hides all elements belonging to existing pastes
2650
         *
2651
         * @name   TopNav.hideAllButtons
2652
         * @function
2653
         */
2654
        me.hideAllButtons = function()
2655
        {
2656
            me.hideViewButtons();
2657
            me.hideCreateButtons();
2658
        }
2659
2660
        /**
2661
         * shows all elements needed when creating a new paste
2662
         *
2663
         * @name   TopNav.showCreateButtons
2664
         * @function
2665
         */
2666
        me.showCreateButtons = function()
2667
        {
2668
            if (createButtonsDisplayed) {
2669
                console.warn('showCreateButtons: create buttons are already displayed');
2670
                return;
2671
            }
2672
2673
            $sendButton.removeClass('hidden');
2674
            $expiration.removeClass('hidden');
2675
            $formatter.removeClass('hidden');
2676
            $burnAfterReadingOption.removeClass('hidden');
2677
            $openDiscussionOption.removeClass('hidden');
2678
            $newButton.removeClass('hidden');
2679
            $password.removeClass('hidden');
2680
            $attach.removeClass('hidden');
2681
2682
            createButtonsDisplayed = true;
2683
        }
2684
2685
        /**
2686
         * shows all elements needed when creating a new paste
2687
         *
2688
         * @name   TopNav.hideCreateButtons
2689
         * @function
2690
         */
2691
        me.hideCreateButtons = function()
2692
        {
2693
            if (!createButtonsDisplayed) {
2694
                console.warn('hideCreateButtons: create buttons are already hidden');
2695
                return;
2696
            }
2697
2698
            $newButton.addClass('hidden');
2699
            $sendButton.addClass('hidden');
2700
            $expiration.addClass('hidden');
2701
            $formatter.addClass('hidden');
2702
            $burnAfterReadingOption.addClass('hidden');
2703
            $openDiscussionOption.addClass('hidden');
2704
            $password.addClass('hidden');
2705
            $attach.addClass('hidden');
2706
2707
            createButtonsDisplayed = false;
2708
        }
2709
2710
        /**
2711
         * only shows the "new paste" button
2712
         *
2713
         * @name   TopNav.showNewPasteButton
2714
         * @function
2715
         */
2716
        me.showNewPasteButton = function()
2717
        {
2718
            $newButton.removeClass('hidden');
2719
        }
2720
2721
        /**
2722
         * only hides the clone button
2723
         *
2724
         * @name   TopNav.hideCloneButton
2725
         * @function
2726
         */
2727
        me.hideCloneButton = function()
2728
        {
2729
            $cloneButton.addClass('hidden');
2730
        }
2731
2732
        /**
2733
         * only hides the raw text button
2734
         *
2735
         * @name   TopNav.hideRawButton
2736
         * @function
2737
         */
2738
        me.hideRawButton = function()
2739
        {
2740
            $rawTextButton.addClass('hidden');
2741
        }
2742
2743
        /**
2744
         * hides the file selector in attachment
2745
         *
2746
         * @name   TopNav.hideFileSelector
2747
         * @function
2748
         */
2749
        me.hideFileSelector = function()
2750
        {
2751
            $fileWrap.addClass('hidden');
2752
        }
2753
2754
2755
        /**
2756
         * shows the custom attachment
2757
         *
2758
         * @name   TopNav.showCustomAttachment
2759
         * @function
2760
         */
2761
        me.showCustomAttachment = function()
2762
        {
2763
            $customAttachment.removeClass('hidden');
2764
        }
2765
2766
        /**
2767
         * collapses the navigation bar if nedded
2768
         *
2769
         * @name   TopNav.collapseBar
2770
         * @function
2771
         */
2772
        me.collapseBar = function()
2773
        {
2774
            var $bar = $('.navbar-toggle');
2775
2776
            // check if bar is expanded
2777
            if ($bar.hasClass('collapse in')) {
2778
                // if so, toggle it
2779
                $bar.click();
2780
            }
2781
        }
2782
2783
        /**
2784
         * returns the currently set expiration time
2785
         *
2786
         * @name   TopNav.getExpiration
2787
         * @function
2788
         * @return {int}
2789
         */
2790
        me.getExpiration = function()
2791
        {
2792
            return pasteExpiration;
2793
        }
2794
2795
        /**
2796
         * returns the currently selected file(s)
2797
         *
2798
         * @name   TopNav.getFileList
2799
         * @function
2800
         * @return {FileList|null}
2801
         */
2802
        me.getFileList = function()
2803
        {
2804
            var $file = $('#file');
2805
2806
            // if no file given, return null
2807
            if (!$file.length || !$file[0].files.length) {
2808
                return null;
2809
            }
2810
            // @TODO is this really necessary
2811
            if (!($file[0].files && $file[0].files[0])) {
2812
                return null;
2813
            }
2814
2815
            return $file[0].files;
2816
        }
2817
2818
        /**
2819
         * returns the state of the burn after reading checkbox
2820
         *
2821
         * @name   TopNav.getExpiration
2822
         * @function
2823
         * @return {bool}
2824
         */
2825
        me.getBurnAfterReading = function()
2826
        {
2827
            return $burnAfterReading.is(':checked');
2828
        }
2829
2830
        /**
2831
         * returns the state of the discussion checkbox
2832
         *
2833
         * @name   TopNav.getOpenDiscussion
2834
         * @function
2835
         * @return {bool}
2836
         */
2837
        me.getOpenDiscussion = function()
2838
        {
2839
            return $openDiscussion.is(':checked');
2840
        }
2841
2842
        /**
2843
         * returns the entered password
2844
         *
2845
         * @name   TopNav.getPassword
2846
         * @function
2847
         * @return {string}
2848
         */
2849
        me.getPassword = function()
2850
        {
2851
            return $passwordInput.val();
2852
        }
2853
2854
        /**
2855
         * returns the element where custom attachments can be placed
2856
         *
2857
         * Used by AttachmentViewer when an attachment is cloned here.
2858
         *
2859
         * @name   TopNav.getCustomAttachment
2860
         * @function
2861
         * @return {jQuery}
2862
         */
2863
        me.getCustomAttachment = function()
2864
        {
2865
            return $customAttachment;
2866
        }
2867
2868
        /**
2869
         * init navigation manager
2870
         *
2871
         * preloads jQuery elements
2872
         *
2873
         * @name   TopNav.init
2874
         * @function
2875
         */
2876
        me.init = function()
2877
        {
2878
            $attach = $('#attach');
2879
            $burnAfterReading = $('#burnafterreading');
2880
            $burnAfterReadingOption = $('#burnafterreadingoption');
2881
            $cloneButton = $('#clonebutton');
2882
            $customAttachment = $('#customattachment');
2883
            $expiration = $('#expiration');
2884
            $fileRemoveButton = $('#fileremovebutton');
2885
            $fileWrap = $('#filewrap');
2886
            $formatter = $('#formatter');
2887
            $newButton = $('#newbutton');
2888
            $openDiscussion = $('#opendiscussion');
2889
            $openDiscussionOption = $('#opendiscussionoption');
2890
            $password = $('#password');
2891
            $passwordInput = $('#passwordinput');
2892
            $rawTextButton = $('#rawtextbutton');
2893
            $sendButton = $('#sendbutton');
2894
            $qrCodeLink = $('#qrcodelink');
2895
2896
            // bootstrap template drop down
2897
            $('#language ul.dropdown-menu li a').click(setLanguage);
2898
            // page template drop down
2899
            $('#language select option').click(setLanguage);
2900
2901
            // bind events
2902
            $burnAfterReading.change(changeBurnAfterReading);
2903
            $openDiscussionOption.change(changeOpenDiscussion);
2904
            $newButton.click(clickNewPaste);
2905
            $sendButton.click(PasteEncrypter.sendPaste);
2906
            $cloneButton.click(Controller.clonePaste);
2907
            $rawTextButton.click(rawText);
2908
            $fileRemoveButton.click(removeAttachment);
2909
            $qrCodeLink.click(displayQrCode);
2910
2911
            // bootstrap template drop downs
2912
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2913
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2914
2915
            // initiate default state of checkboxes
2916
            changeBurnAfterReading();
2917
            changeOpenDiscussion();
2918
2919
            // get default value from template or fall back to set value
2920
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2921
        }
2922
2923
        return me;
2924
    })(window, document);
2925
2926
    /**
2927
     * Responsible for AJAX requests, transparently handles encryption…
2928
     *
2929
     * @name   Uploader
2930
     * @class
2931
     */
2932
    var Uploader = (function () {
2933
        var me = {};
2934
2935
        var successFunc = null,
2936
            failureFunc = null,
2937
            url,
2938
            data,
2939
            symmetricKey,
2940
            password;
2941
2942
        /**
2943
         * public variable ('constant') for errors to prevent magic numbers
2944
         *
2945
         * @name   Uploader.error
2946
         * @readonly
2947
         * @enum   {Object}
2948
         */
2949
        me.error = {
2950
            okay: 0,
2951
            custom: 1,
2952
            unknown: 2,
2953
            serverError: 3
2954
        };
2955
2956
        /**
2957
         * ajaxHeaders to send in AJAX requests
2958
         *
2959
         * @name   Uploader.ajaxHeaders
2960
         * @private
2961
         * @readonly
2962
         * @enum   {Object}
2963
         */
2964
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2965
2966
        /**
2967
         * called after successful upload
2968
         *
2969
         * @name   Uploader.checkCryptParameters
2970
         * @private
2971
         * @function
2972
         * @throws {string}
2973
         */
2974
        function checkCryptParameters()
2975
        {
2976
            // workaround for this nasty 'bug' in ECMAScript
2977
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
2978
            var typeOfKey = typeof symmetricKey;
2979
            if (symmetricKey === null) {
2980
                typeOfKey = 'null';
2981
            }
2982
2983
            // in case of missing preparation, throw error
2984
            switch (typeOfKey) {
2985
                case 'string':
2986
                    // already set, all right
2987
                    return;
2988
                case 'null':
2989
                    // needs to be generated auto-generate
2990
                    symmetricKey = CryptTool.getSymmetricKey();
2991
                    break;
2992
                default:
2993
                    console.error('current invalid symmetricKey:', symmetricKey);
2994
                    throw 'symmetricKey is invalid, probably the module was not prepared';
2995
            }
2996
            // password is optional
2997
        }
2998
2999
        /**
3000
         * called after successful upload
3001
         *
3002
         * @name   Uploader.success
3003
         * @private
3004
         * @function
3005
         * @param {int} status
3006
         * @param {int} result - optional
3007
         */
3008
        function success(status, result)
3009
        {
3010
            // add useful data to result
3011
            result.encryptionKey = symmetricKey;
3012
            result.requestData = data;
3013
3014
            if (successFunc !== null) {
3015
                successFunc(status, result);
3016
            }
3017
        }
3018
3019
        /**
3020
         * called after a upload failure
3021
         *
3022
         * @name   Uploader.fail
3023
         * @private
3024
         * @function
3025
         * @param {int} status - internal code
3026
         * @param {int} result - original error code
3027
         */
3028
        function fail(status, result)
3029
        {
3030
            if (failureFunc !== null) {
3031
                failureFunc(status, result);
3032
            }
3033
        }
3034
3035
        /**
3036
         * actually uploads the data
3037
         *
3038
         * @name   Uploader.run
3039
         * @function
3040
         */
3041
        me.run = function()
3042
        {
3043
            $.ajax({
3044
                type: 'POST',
3045
                url: url,
3046
                data: data,
3047
                dataType: 'json',
3048
                headers: ajaxHeaders,
3049
                success: function(result) {
3050
                    if (result.status === 0) {
3051
                        success(0, result);
3052
                    } else if (result.status === 1) {
3053
                        fail(1, result);
3054
                    } else {
3055
                        fail(2, result);
3056
                    }
3057
                }
3058
            })
3059
            .fail(function(jqXHR, textStatus, errorThrown) {
3060
                console.error(textStatus, errorThrown);
3061
                fail(3, jqXHR);
3062
            });
3063
        }
3064
3065
        /**
3066
         * set success function
3067
         *
3068
         * @name   Uploader.setUrl
3069
         * @function
3070
         * @param {function} newUrl
3071
         */
3072
        me.setUrl = function(newUrl)
3073
        {
3074
            url = newUrl;
3075
        }
3076
3077
        /**
3078
         * sets the password to use (first value) and optionally also the
3079
         * encryption key (not recommend, it is automatically generated).
3080
         *
3081
         * Note: Call this after prepare() as prepare() resets these values.
3082
         *
3083
         * @name   Uploader.setCryptValues
3084
         * @function
3085
         * @param {string} newPassword
3086
         * @param {string} newKey       - optional
3087
         */
3088
        me.setCryptParameters = function(newPassword, newKey)
3089
        {
3090
            password = newPassword;
3091
3092
            if (typeof newKey !== 'undefined') {
3093
                symmetricKey = newKey;
3094
            }
3095
        }
3096
3097
        /**
3098
         * set success function
3099
         *
3100
         * @name   Uploader.setSuccess
3101
         * @function
3102
         * @param {function} func
3103
         */
3104
        me.setSuccess = function(func)
3105
        {
3106
            successFunc = func;
3107
        }
3108
3109
        /**
3110
         * set failure function
3111
         *
3112
         * @name   Uploader.setFailure
3113
         * @function
3114
         * @param {function} func
3115
         */
3116
        me.setFailure = function(func)
3117
        {
3118
            failureFunc = func;
3119
        }
3120
3121
        /**
3122
         * prepares a new upload
3123
         *
3124
         * Call this when doing a new upload to reset any data from potential
3125
         * previous uploads. Must be called before any other method of this
3126
         * module.
3127
         *
3128
         * @name   Uploader.prepare
3129
         * @function
3130
         * @return {object}
3131
         */
3132
        me.prepare = function()
3133
        {
3134
            // entropy should already be checked!
3135
3136
            // reset password
3137
            password = '';
3138
3139
            // reset key, so it a new one is generated when it is used
3140
            symmetricKey = null;
3141
3142
            // reset data
3143
            successFunc = null;
3144
            failureFunc = null;
3145
            url = Helper.baseUri();
3146
            data = {};
3147
        }
3148
3149
        /**
3150
         * encrypts and sets the data
3151
         *
3152
         * @name   Uploader.setData
3153
         * @function
3154
         * @param {string} index
3155
         * @param {mixed} element
3156
         */
3157
        me.setData = function(index, element)
3158
        {
3159
            checkCryptParameters();
3160
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3161
        }
3162
3163
        /**
3164
         * set the additional metadata to send unencrypted
3165
         *
3166
         * @name   Uploader.setUnencryptedData
3167
         * @function
3168
         * @param {string} index
3169
         * @param {mixed} element
3170
         */
3171
        me.setUnencryptedData = function(index, element)
3172
        {
3173
            data[index] = element;
3174
        }
3175
3176
        /**
3177
         * set the additional metadata to send unencrypted passed at once
3178
         *
3179
         * @name   Uploader.setUnencryptedData
3180
         * @function
3181
         * @param {object} newData
3182
         */
3183
        me.setUnencryptedBulkData = function(newData)
3184
        {
3185
            $.extend(data, newData);
3186
        }
3187
3188
        /**
3189
         * Helper, which parses shows a general error message based on the result of the Uploader
3190
         *
3191
         * @name    Uploader.parseUploadError
3192
         * @function
3193
         * @param {int} status
3194
         * @param {object} data
3195
         * @param {string} doThisThing - a human description of the action, which was tried
3196
         * @return {array}
3197
         */
3198
        me.parseUploadError = function(status, data, doThisThing) {
3199
            var errorArray;
3200
3201
            switch (status) {
3202
                case me.error['custom']:
3203
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3204
                    break;
3205
                case me.error['unknown']:
3206
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3207
                    break;
3208
                case me.error['serverError']:
3209
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3210
                    break;
3211
                default:
3212
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3213
                    break;
3214
            }
3215
3216
            return errorArray;
3217
        }
3218
3219
        /**
3220
         * init Uploader
3221
         *
3222
         * @name   Uploader.init
3223
         * @function
3224
         */
3225
        me.init = function()
3226
        {
3227
            // nothing yet
3228
        }
3229
3230
        return me;
3231
    })();
3232
3233
    /**
3234
     * (controller) Responsible for encrypting paste and sending it to server.
3235
     *
3236
     * Does upload, encryption is done transparently by Uploader.
3237
     *
3238
     * @name PasteEncrypter
3239
     * @class
3240
     */
3241
    var PasteEncrypter = (function () {
3242
        var me = {};
3243
3244
        var requirementsChecked = false;
3245
3246
        /**
3247
         * checks whether there is a suitable amount of entrophy
3248
         *
3249
         * @name PasteEncrypter.checkRequirements
3250
         * @private
3251
         * @function
3252
         * @param {function} retryCallback - the callback to execute to retry the upload
3253
         * @return {bool}
3254
         */
3255
        function checkRequirements(retryCallback) {
3256
            // skip double requirement checks
3257
            if (requirementsChecked === true) {
3258
                return true;
3259
            }
3260
3261
            if (!CryptTool.isEntropyReady()) {
3262
                // display a message and wait
3263
                Alert.showStatus('Please move your mouse for more entropy…');
3264
3265
                CryptTool.addEntropySeedListener(retryCallback);
3266
                return false;
3267
            }
3268
3269
            requirementsChecked = true;
3270
3271
            return true;
3272
        }
3273
3274
        /**
3275
         * called after successful paste upload
3276
         *
3277
         * @name PasteEncrypter.showCreatedPaste
3278
         * @private
3279
         * @function
3280
         * @param {int} status
3281
         * @param {object} data
3282
         */
3283
        function showCreatedPaste(status, data) {
3284
            Alert.hideLoading();
3285
3286
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3287
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3288
3289
            Alert.hideMessages();
3290
3291
            // show notification
3292
            PasteStatus.createPasteNotification(url, deleteUrl)
3293
3294
            // show new URL in browser bar
3295
            history.pushState({type: 'newpaste'}, document.title, url);
3296
3297
            TopNav.showViewButtons();
3298
            TopNav.hideRawButton();
3299
            Editor.hide();
3300
3301
            // parse and show text
3302
            // (preparation already done in me.sendPaste())
3303
            PasteViewer.run();
3304
        }
3305
3306
        /**
3307
         * called after successful comment upload
3308
         *
3309
         * @name PasteEncrypter.showUploadedComment
3310
         * @private
3311
         * @function
3312
         * @param {int} status
3313
         * @param {object} data
3314
         */
3315
        function showUploadedComment(status, data) {
3316
            // show success message
3317
            // Alert.showStatus('Comment posted.');
3318
3319
            // reload paste
3320
            Controller.refreshPaste(function () {
3321
                // highlight sent comment
3322
                DiscussionViewer.highlightComment(data.id, true);
3323
                // reset error handler
3324
                Alert.setCustomHandler(null);
3325
            });
3326
        }
3327
3328
        /**
3329
         * adds attachments to the Uploader
3330
         *
3331
         * @name PasteEncrypter.encryptAttachments
3332
         * @private
3333
         * @function
3334
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3335
         * @param {function} callback - excuted when action is successful
3336
         */
3337
        function encryptAttachments(file, callback) {
3338
            if (typeof file !== 'undefined' && file !== null) {
3339
                // check file reader requirements for upload
3340
                if (typeof FileReader === 'undefined') {
3341
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3342
                    // cancels process as it does not execute callback
3343
                    return;
3344
                }
3345
3346
                var reader = new FileReader();
3347
3348
                // closure to capture the file information
3349
                reader.onload = function(event) {
3350
                    Uploader.setData('attachment', event.target.result);
3351
                    Uploader.setData('attachmentname', file.name);
3352
3353
                    // run callback
3354
                    return callback();
3355
                }
3356
3357
                // actually read first file
3358
                reader.readAsDataURL(file);
3359
            } else if (AttachmentViewer.hasAttachment()) {
3360
                // fall back to cloned part
3361
                var attachment = AttachmentViewer.getAttachment();
3362
3363
                Uploader.setData('attachment', attachment[0]);
3364
                Uploader.setData('attachmentname', attachment[1]);
3365
                return callback();
3366
            } else {
3367
                // if there are no attachments, this is of course still successful
3368
                return callback();
3369
            }
3370
        }
3371
3372
        /**
3373
         * send a reply in a discussion
3374
         *
3375
         * @name   PasteEncrypter.sendComment
3376
         * @function
3377
         */
3378
        me.sendComment = function()
3379
        {
3380
            Alert.hideMessages();
3381
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3382
3383
            // UI loading state
3384
            TopNav.hideAllButtons();
3385
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3386
3387
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3388
            var replyData = DiscussionViewer.getReplyData(),
3389
                plainText = replyData[0],
3390
                nickname = replyData[1],
3391
                parentid = DiscussionViewer.getReplyCommentId();
3392
3393
            // do not send if there is no data
3394
            if (plainText.length === 0) {
3395
                // revert loading status…
3396
                Alert.hideLoading();
3397
                Alert.setCustomHandler(null);
3398
                TopNav.showViewButtons();
3399
                return;
3400
            }
3401
3402
            // check entropy
3403
            if (!checkRequirements(function () {
3404
                me.sendComment();
3405
            })) {
3406
                return; // to prevent multiple executions
3407
            }
3408
            Alert.showLoading(null, 10);
3409
3410
            // prepare Uploader
3411
            Uploader.prepare();
3412
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3413
3414
            // set success/fail functions
3415
            Uploader.setSuccess(showUploadedComment);
3416
            Uploader.setFailure(function (status, data) {
3417
                // revert loading status…
3418
                Alert.hideLoading();
3419
                TopNav.showViewButtons();
3420
3421
                // show error message
3422
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3423
3424
                // reset error handler
3425
                Alert.setCustomHandler(null);
3426
            });
3427
3428
            // fill it with unencrypted params
3429
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3430
            if (typeof parentid === 'undefined') {
3431
                // if parent id is not set, this is the top-most comment, so use
3432
                // paste id as parent @TODO is this really good?
3433
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3434
            } else {
3435
                Uploader.setUnencryptedData('parentid', parentid);
3436
            }
3437
3438
            // encrypt data
3439
            Uploader.setData('data', plainText);
3440
3441
            if (nickname.length > 0) {
3442
                Uploader.setData('nickname', nickname);
3443
            }
3444
3445
            Uploader.run();
3446
        }
3447
3448
        /**
3449
         * sends a new paste to server
3450
         *
3451
         * @name   PasteEncrypter.sendPaste
3452
         * @function
3453
         */
3454
        me.sendPaste = function()
3455
        {
3456
            // hide previous (error) messages
3457
            Controller.hideStatusMessages();
3458
3459
            // UI loading state
3460
            TopNav.hideAllButtons();
3461
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3462
            TopNav.collapseBar();
3463
3464
            // get data
3465
            var plainText = Editor.getText(),
3466
                format = PasteViewer.getFormat(),
3467
                files = TopNav.getFileList();
3468
3469
            // do not send if there is no data
3470
            if (plainText.length === 0 && files === null) {
3471
                // revert loading status…
3472
                Alert.hideLoading();
3473
                TopNav.showCreateButtons();
3474
                return;
3475
            }
3476
3477
            Alert.showLoading(null, 10);
3478
3479
            // check entropy
3480
            if (!checkRequirements(function () {
3481
                me.sendPaste();
3482
            })) {
3483
                return; // to prevent multiple executions
3484
            }
3485
3486
            // prepare Uploader
3487
            Uploader.prepare();
3488
            Uploader.setCryptParameters(TopNav.getPassword());
3489
3490
            // set success/fail functions
3491
            Uploader.setSuccess(showCreatedPaste);
3492
            Uploader.setFailure(function (status, data) {
3493
                // revert loading status…
3494
                Alert.hideLoading();
3495
                TopNav.showCreateButtons();
3496
3497
                // show error message
3498
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3499
            });
3500
3501
            // fill it with unencrypted submitted options
3502
            Uploader.setUnencryptedBulkData({
3503
                expire:           TopNav.getExpiration(),
3504
                formatter:        format,
3505
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3506
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3507
            });
3508
3509
            // prepare PasteViewer for later preview
3510
            PasteViewer.setText(plainText);
3511
            PasteViewer.setFormat(format);
3512
3513
            // encrypt cipher data
3514
            Uploader.setData('data', plainText);
3515
3516
            // encrypt attachments
3517
            encryptAttachments(
3518
                files === null ? null : files[0],
3519
                function () {
3520
                    // send data
3521
                    Uploader.run();
3522
                }
3523
            );
3524
        }
3525
3526
        /**
3527
         * initialize
3528
         *
3529
         * @name   PasteEncrypter.init
3530
         * @function
3531
         */
3532
        me.init = function()
3533
        {
3534
            // nothing yet
3535
        }
3536
3537
        return me;
3538
    })();
3539
3540
    /**
3541
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3542
     *
3543
     * Only decryption, no download.
3544
     *
3545
     * @name PasteDecrypter
3546
     * @class
3547
     */
3548
    var PasteDecrypter = (function () {
3549
        var me = {};
3550
3551
        /**
3552
         * decrypt data or prompts for password in cvase of failure
3553
         *
3554
         * @name   PasteDecrypter.decryptOrPromptPassword
3555
         * @private
3556
         * @function
3557
         * @param  {string} key
3558
         * @param  {string} password - optional, may be an empty string
3559
         * @param  {string} cipherdata
3560
         * @throws {string}
3561
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3562
         */
3563
        function decryptOrPromptPassword(key, password, cipherdata)
3564
        {
3565
            // try decryption without password
3566
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3567
3568
            // if it fails, request password
3569
            if (plaindata.length === 0 && password.length === 0) {
3570
                // try to get cached password first
3571
                password = Prompt.getPassword();
3572
3573
                // if password is there, re-try
3574
                if (password.length === 0) {
3575
                    password = Prompt.requestPassword();
3576
                }
3577
                // recursive
3578
                // note: an infinite loop is prevented as the previous if
3579
                // clause checks whether a password is already set and ignores
3580
                // errors when a password has been passed
3581
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3582
            }
3583
3584
            // if all tries failed, we can only return an error
3585
            if (plaindata.length === 0) {
3586
                throw 'failed to decipher data';
3587
            }
3588
3589
            return plaindata;
3590
        }
3591
3592
        /**
3593
         * decrypt the actual paste text
3594
         *
3595
         * @name   PasteDecrypter.decryptOrPromptPassword
3596
         * @private
3597
         * @function
3598
         * @param  {object} paste - paste data in object form
3599
         * @param  {string} key
3600
         * @param  {string} password
3601
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3602
         * @return {bool} whether action was successful
3603
         * @throws {string}
3604
         */
3605
        function decryptPaste(paste, key, password, ignoreError)
3606
        {
3607
            var plaintext
3608
            if (ignoreError === true) {
3609
                plaintext = CryptTool.decipher(key, password, paste.data);
3610
            } else {
3611
                try {
3612
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3613
                } catch (err) {
3614
                    throw 'failed to decipher paste text: ' + err
3615
                }
3616
                if (plaintext === false) {
3617
                    return false;
3618
                }
3619
            }
3620
3621
            // on success show paste
3622
            PasteViewer.setFormat(paste.meta.formatter);
3623
            PasteViewer.setText(plaintext);
3624
            // trigger to show the text (attachment loaded afterwards)
3625
            PasteViewer.run();
3626
3627
            return true;
3628
        }
3629
3630
        /**
3631
         * decrypts any attachment
3632
         *
3633
         * @name   PasteDecrypter.decryptAttachment
3634
         * @private
3635
         * @function
3636
         * @param  {object} paste - paste data in object form
3637
         * @param  {string} key
3638
         * @param  {string} password
3639
         * @return {bool} whether action was successful
3640
         * @throws {string}
3641
         */
3642
        function decryptAttachment(paste, key, password)
3643
        {
3644
            // decrypt attachment
3645
            try {
3646
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3647
            } catch (err) {
3648
                throw 'failed to decipher attachment: ' + err
3649
            }
3650
            if (attachment === false) {
3651
                return false;
3652
            }
3653
3654
            // decrypt attachment name
3655
            var attachmentName;
3656
            if (paste.attachmentname) {
3657
                try {
3658
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3659
                } catch (err) {
3660
                    throw 'failed to decipher attachment name: ' + err
3661
                }
3662
                if (attachmentName === false) {
3663
                    return false;
3664
                }
3665
            }
3666
3667
            AttachmentViewer.setAttachment(attachment, attachmentName);
3668
            AttachmentViewer.showAttachment();
3669
3670
            return true;
3671
        }
3672
3673
        /**
3674
         * decrypts all comments and shows them
3675
         *
3676
         * @name   PasteDecrypter.decryptComments
3677
         * @private
3678
         * @function
3679
         * @param  {object} paste - paste data in object form
3680
         * @param  {string} key
3681
         * @param  {string} password
3682
         * @return {bool} whether action was successful
3683
         */
3684
        function decryptComments(paste, key, password)
3685
        {
3686
            // remove potentially previous discussion
3687
            DiscussionViewer.prepareNewDisucssion();
3688
3689
            // iterate over comments
3690
            for (var i = 0; i < paste.comments.length; ++i) {
3691
                var comment = paste.comments[i];
3692
3693
                DiscussionViewer.addComment(
3694
                    comment,
3695
                    CryptTool.decipher(key, password, comment.data),
3696
                    CryptTool.decipher(key, password, comment.meta.nickname)
3697
                );
3698
            }
3699
3700
            DiscussionViewer.finishDiscussion();
3701
            DiscussionViewer.showDiscussion();
3702
            return true;
3703
        }
3704
3705
        /**
3706
         * show decrypted text in the display area, including discussion (if open)
3707
         *
3708
         * @name   PasteDecrypter.run
3709
         * @function
3710
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3711
         */
3712
        me.run = function(paste)
3713
        {
3714
            Alert.hideMessages();
3715
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3716
3717
            if (typeof paste === 'undefined') {
3718
                paste = $.parseJSON(Model.getCipherData());
3719
            }
3720
3721
            var key = Model.getPasteKey(),
3722
                password = Prompt.getPassword();
3723
3724
            if (PasteViewer.isPrettyPrinted()) {
3725
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3726
                return;
3727
            }
3728
3729
            // try to decrypt the paste
3730
            try {
3731
                // decrypt attachments
3732
                if (paste.attachment) {
3733
                    // try to decrypt paste and if it fails (because the password is
3734
                    // missing) return to let JS continue and wait for user
3735
                    if (!decryptAttachment(paste, key, password)) {
3736
                        return;
3737
                    }
3738
                    // ignore empty paste, as this is allowed when pasting attachments
3739
                    decryptPaste(paste, key, password, true);
3740
                } else {
3741
                    decryptPaste(paste, key, password);
3742
                }
3743
3744
3745
                // shows the remaining time (until) deletion
3746
                PasteStatus.showRemainingTime(paste.meta);
3747
3748
                // if the discussion is opened on this paste, display it
3749
                if (paste.meta.opendiscussion) {
3750
                    decryptComments(paste, key, password);
3751
                }
3752
3753
                Alert.hideLoading();
3754
                TopNav.showViewButtons();
3755
            } catch(err) {
3756
                Alert.hideLoading();
3757
3758
                // log and show error
3759
                console.error(err);
3760
                Alert.showError('Could not decrypt data (Wrong key?)');
3761
            }
3762
        }
3763
3764
        /**
3765
         * initialize
3766
         *
3767
         * @name   PasteDecrypter.init
3768
         * @function
3769
         */
3770
        me.init = function()
3771
        {
3772
            // nothing yet
3773
        }
3774
3775
        return me;
3776
    })();
3777
3778
    /**
3779
     * (controller) main PrivateBin logic
3780
     *
3781
     * @name   Controller
3782
     * @param  {object} window
3783
     * @param  {object} document
3784
     * @class
3785
     */
3786
    var Controller = (function (window, document) {
3787
        var me = {};
3788
3789
        /**
3790
         * hides all status messages no matter which module showed them
3791
         *
3792
         * @name   Controller.hideStatusMessages
3793
         * @function
3794
         */
3795
        me.hideStatusMessages = function()
3796
        {
3797
            PasteStatus.hideMessages();
3798
            Alert.hideMessages();
3799
        }
3800
3801
        /**
3802
         * creates a new paste
3803
         *
3804
         * @name   Controller.newPaste
3805
         * @function
3806
         */
3807
        me.newPaste = function()
3808
        {
3809
            // Important: This *must not* run Alert.hideMessages() as previous
3810
            // errors from viewing a paste should be shown.
3811
            TopNav.hideAllButtons();
3812
            Alert.showLoading('Preparing new paste…', 0, 'time');
3813
3814
            PasteStatus.hideMessages();
3815
            PasteViewer.hide();
3816
            Editor.resetInput();
3817
            Editor.show();
3818
            Editor.focusInput();
3819
3820
            TopNav.showCreateButtons();
3821
            Alert.hideLoading();
3822
        }
3823
3824
        /**
3825
         * shows the loaded paste
3826
         *
3827
         * @name   Controller.showPaste
3828
         * @function
3829
         */
3830
        me.showPaste = function()
3831
        {
3832
            try {
3833
                Model.getPasteId();
3834
                Model.getPasteKey();
3835
            } catch (err) {
3836
                console.error(err);
3837
3838
                // missing decryption key (or paste ID) in URL?
3839
                if (window.location.hash.length === 0) {
3840
                    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?)');
3841
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3842
                    return;
3843
                }
3844
            }
3845
3846
            // show proper elements on screen
3847
            PasteDecrypter.run();
3848
        }
3849
3850
        /**
3851
         * refreshes the loaded paste to show potential new data
3852
         *
3853
         * @name   Controller.refreshPaste
3854
         * @function
3855
         * @param  {function} callback
3856
         */
3857
        me.refreshPaste = function(callback)
3858
        {
3859
            // save window position to restore it later
3860
            var orgPosition = $(window).scrollTop();
3861
3862
            Uploader.prepare();
3863
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3864
3865
            Uploader.setFailure(function (status, data) {
3866
                // revert loading status…
3867
                Alert.hideLoading();
3868
                TopNav.showViewButtons();
3869
3870
                // show error message
3871
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3872
            })
3873
            Uploader.setSuccess(function (status, data) {
3874
                PasteDecrypter.run(data);
3875
3876
                // restore position
3877
                window.scrollTo(0, orgPosition);
3878
3879
                callback();
3880
            })
3881
            Uploader.run();
3882
        }
3883
3884
        /**
3885
         * clone the current paste
3886
         *
3887
         * @name   Controller.clonePaste
3888
         * @function
3889
         * @param  {Event} event
3890
         */
3891
        me.clonePaste = function(event)
3892
        {
3893
            TopNav.collapseBar();
3894
            TopNav.hideAllButtons();
3895
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3896
3897
            // hide messages from previous paste
3898
            me.hideStatusMessages();
3899
3900
            // erase the id and the key in url
3901
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3902
3903
            if (AttachmentViewer.hasAttachment()) {
3904
                AttachmentViewer.moveAttachmentTo(
3905
                    TopNav.getCustomAttachment(),
3906
                    'Cloned: \'%s\''
3907
                );
3908
                TopNav.hideFileSelector();
3909
                AttachmentViewer.hideAttachment();
3910
                // NOTE: it also looks nice without removing the attachment
3911
                // but for a consistent display we remove it…
3912
                AttachmentViewer.hideAttachmentPreview();
3913
                TopNav.showCustomAttachment();
3914
3915
                // show another status message to make the user aware that the
3916
                // file was cloned too!
3917
                Alert.showStatus(
3918
                    [
3919
                        'The cloned file \'%s\' was attached to this paste.',
3920
                        AttachmentViewer.getAttachment()[1]
3921
                    ], 'copy', true, true);
3922
            }
3923
3924
            Editor.setText(PasteViewer.getText())
3925
            PasteViewer.hide();
3926
            Editor.show();
3927
3928
            Alert.hideLoading();
3929
            TopNav.showCreateButtons();
3930
        }
3931
3932
        /**
3933
         * removes a saved paste
3934
         *
3935
         * @name   Controller.removePaste
3936
         * @function
3937
         * @param  {string} pasteId
3938
         * @param  {string} deleteToken
3939
         */
3940
        me.removePaste = function(pasteId, deleteToken) {
3941
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3942
            // so we use a POST request
3943
            Uploader.prepare();
3944
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3945
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3946
3947
            Uploader.setFailure(function () {
3948
                Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3949
            })
3950
            Uploader.run();
3951
        }
3952
3953
        /**
3954
         * application start
3955
         *
3956
         * @name   Controller.init
3957
         * @function
3958
         */
3959
        me.init = function()
3960
        {
3961
            // first load translations
3962
            I18n.loadTranslations();
3963
3964
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
3965
3966
            // initialize other modules/"classes"
3967
            Alert.init();
3968
            Model.init();
3969
            AttachmentViewer.init();
3970
            DiscussionViewer.init();
3971
            Editor.init();
3972
            PasteDecrypter.init();
3973
            PasteEncrypter.init();
3974
            PasteStatus.init();
3975
            PasteViewer.init();
3976
            Prompt.init();
3977
            TopNav.init();
3978
            UiHelper.init();
3979
            Uploader.init();
3980
3981
            // display an existing paste
3982
            if (Model.hasCipherData()) {
3983
                return me.showPaste();
3984
            }
3985
3986
            // otherwise create a new paste
3987
            me.newPaste();
3988
        }
3989
3990
        return me;
3991
    })(window, document);
3992
3993
    return {
3994
        Helper: Helper,
3995
        I18n: I18n,
3996
        CryptTool: CryptTool,
3997
        Model: Model,
3998
        UiHelper: UiHelper,
3999
        Alert: Alert,
4000
        PasteStatus: PasteStatus,
4001
        Prompt: Prompt,
4002
        Editor: Editor,
4003
        PasteViewer: PasteViewer,
4004
        AttachmentViewer: AttachmentViewer,
4005
        DiscussionViewer: DiscussionViewer,
4006
        TopNav: TopNav,
4007
        Uploader: Uploader,
4008
        PasteEncrypter: PasteEncrypter,
4009
        PasteDecrypter: PasteDecrypter,
4010
        Controller: Controller
4011
    };
4012
}(jQuery, sjcl, Base64, RawDeflate);
4013