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

js/privatebin.js (13 issues)

Severity
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 returns 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)
0 ignored issues
show
The parameter autoclose 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...
The parameter dismissable 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...
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)
0 ignored issues
show
The parameter dismissable 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...
The parameter autoclose 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...
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)
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...
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)
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...
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
            // escape HTML entities, link URLs, sanitize
1704
            var escapedLinkedText = Helper.urls2links(
1705
                    $('<div />').text(text).html()
1706
                ),
1707
                sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText);
1708
            $plainText.html(sanitizedLinkedText);
1709
            $prettyPrint.html(sanitizedLinkedText);
1710
1711
            switch (format) {
1712
                case 'markdown':
1713
                    var converter = new showdown.Converter({
1714
                        strikethrough: true,
1715
                        tables: true,
1716
                        tablesHeaderId: true
1717
                    });
1718
                    // let showdown convert the HTML and sanitize HTML *afterwards*!
1719
                    $plainText.html(
1720
                        DOMPurify.sanitize(converter.makeHtml(text))
1721
                    );
1722
                    // add table classes from bootstrap css
1723
                    $plainText.find('table').addClass('table-condensed table-bordered');
1724
                    break;
1725
                case 'syntaxhighlighting':
1726
                    // yes, this is really needed to initialize the environment
1727
                    if (typeof prettyPrint === 'function')
1728
                    {
1729
                        prettyPrint();
1730
                    }
1731
1732
                    $prettyPrint.html(
1733
                        DOMPurify.sanitize(
1734
                            prettyPrintOne(escapedLinkedText, null, true)
1735
                        )
1736
                    );
1737
                    // fall through, as the rest is the same
1738
                default: // = 'plaintext'
1739
                    $prettyPrint.css('white-space', 'pre-wrap');
1740
                    $prettyPrint.css('word-break', 'normal');
1741
                    $prettyPrint.removeClass('prettyprint');
1742
            }
1743
        }
1744
1745
        /**
1746
         * displays the paste
1747
         *
1748
         * @name   PasteViewer.showPaste
1749
         * @private
1750
         * @function
1751
         */
1752
        function showPaste()
1753
        {
1754
            // instead of "nothing" better display a placeholder
1755
            if (text === '') {
1756
                $placeholder.removeClass('hidden');
1757
                return;
1758
            }
1759
            // otherwise hide the placeholder
1760
            $placeholder.addClass('hidden');
1761
1762
            switch (format) {
1763
                case 'markdown':
1764
                    $plainText.removeClass('hidden');
1765
                    $prettyMessage.addClass('hidden');
1766
                    break;
1767
                default:
1768
                    $plainText.addClass('hidden');
1769
                    $prettyMessage.removeClass('hidden');
1770
                    break;
1771
            }
1772
        }
1773
1774
        /**
1775
         * sets the format in which the text is shown
1776
         *
1777
         * @name   PasteViewer.setFormat
1778
         * @function
1779
         * @param {string} newFormat the new format
1780
         */
1781
        me.setFormat = function(newFormat)
1782
        {
1783
            // skip if there is no update
1784
            if (format === newFormat) {
1785
                return;
1786
            }
1787
1788
            // needs to update display too, if we switch from or to Markdown
1789
            if (format === 'markdown' || newFormat === 'markdown') {
1790
                isDisplayed = false;
1791
            }
1792
1793
            format = newFormat;
1794
            isChanged = true;
1795
        };
1796
1797
        /**
1798
         * returns the current format
1799
         *
1800
         * @name   PasteViewer.getFormat
1801
         * @function
1802
         * @return {string}
1803
         */
1804
        me.getFormat = function()
1805
        {
1806
            return format;
1807
        };
1808
1809
        /**
1810
         * returns whether the current view is pretty printed
1811
         *
1812
         * @name   PasteViewer.isPrettyPrinted
1813
         * @function
1814
         * @return {bool}
1815
         */
1816
        me.isPrettyPrinted = function()
1817
        {
1818
            return $prettyPrint.hasClass('prettyprinted');
1819
        };
1820
1821
        /**
1822
         * sets the text to show
1823
         *
1824
         * @name   PasteViewer.setText
1825
         * @function
1826
         * @param {string} newText the text to show
1827
         */
1828
        me.setText = function(newText)
1829
        {
1830
            if (text !== newText) {
1831
                text = newText;
1832
                isChanged = true;
1833
            }
1834
        };
1835
1836
        /**
1837
         * gets the current cached text
1838
         *
1839
         * @name   PasteViewer.getText
1840
         * @function
1841
         * @return {string}
1842
         */
1843
        me.getText = function()
1844
        {
1845
            return text;
1846
        };
1847
1848
        /**
1849
         * show/update the parsed text (preview)
1850
         *
1851
         * @name   PasteViewer.run
1852
         * @function
1853
         */
1854
        me.run = function()
1855
        {
1856
            if (isChanged) {
1857
                parsePaste();
1858
                isChanged = false;
1859
            }
1860
1861
            if (!isDisplayed) {
1862
                showPaste();
1863
                isDisplayed = true;
1864
            }
1865
        };
1866
1867
        /**
1868
         * hide parsed text (preview)
1869
         *
1870
         * @name   PasteViewer.hide
1871
         * @function
1872
         */
1873
        me.hide = function()
1874
        {
1875
            if (!isDisplayed) {
1876
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1877
            }
1878
1879
            $plainText.addClass('hidden');
1880
            $prettyMessage.addClass('hidden');
1881
            $placeholder.addClass('hidden');
1882
1883
            isDisplayed = false;
1884
        };
1885
1886
        /**
1887
         * init status manager
1888
         *
1889
         * preloads jQuery elements
1890
         *
1891
         * @name   PasteViewer.init
1892
         * @function
1893
         */
1894
        me.init = function()
1895
        {
1896
            $placeholder = $('#placeholder');
1897
            $plainText = $('#plaintext');
1898
            $prettyMessage = $('#prettymessage');
1899
            $prettyPrint = $('#prettyprint');
1900
1901
            // check requirements
1902
            if (typeof prettyPrintOne !== 'function') {
1903
                Alert.showError([
1904
                    'The library %s is not available. This may cause display errors.',
1905
                    'pretty print'
1906
                ]);
1907
            }
1908
            if (typeof showdown !== 'object') {
1909
                Alert.showError([
1910
                    'The library %s is not available. This may cause display errors.',
1911
                    'showdown'
1912
                ]);
1913
            }
1914
1915
            // get default option from template/HTML or fall back to set value
1916
            format = Model.getFormatDefault() || format;
1917
            text = '';
1918
            isDisplayed = false;
1919
            isChanged = true;
1920
        };
1921
1922
        return me;
1923
    })();
1924
1925
    /**
1926
     * (view) Show attachment and preview if possible
1927
     *
1928
     * @name   AttachmentViewer
1929
     * @class
1930
     */
1931
    var AttachmentViewer = (function () {
1932
        var me = {};
1933
1934
        var $attachmentLink,
1935
            $attachmentPreview,
1936
            $attachment;
1937
1938
        var attachmentHasPreview = false;
1939
1940
        /**
1941
         * sets the attachment but does not yet show it
1942
         *
1943
         * @name   AttachmentViewer.setAttachment
1944
         * @function
1945
         * @param {string} attachmentData - base64-encoded data of file
1946
         * @param {string} fileName - optional, file name
1947
         */
1948
        me.setAttachment = function(attachmentData, fileName)
1949
        {
1950
            var imagePrefix = 'data:image/';
1951
1952
            $attachmentLink.attr('href', attachmentData);
1953
            if (typeof fileName !== 'undefined') {
1954
                $attachmentLink.attr('download', fileName);
1955
            }
1956
1957
            // if the attachment is an image, display it
1958
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
1959
                $attachmentPreview.html(
1960
                    $(document.createElement('img'))
1961
                        .attr('src', attachmentData)
1962
                        .attr('class', 'img-thumbnail')
1963
                );
1964
                attachmentHasPreview = true;
1965
            }
1966
        };
1967
1968
        /**
1969
         * displays the attachment
1970
         *
1971
         * @name AttachmentViewer.showAttachment
1972
         * @function
1973
         */
1974
        me.showAttachment = function()
1975
        {
1976
            $attachment.removeClass('hidden');
1977
1978
            if (attachmentHasPreview) {
1979
                $attachmentPreview.removeClass('hidden');
1980
            }
1981
        };
1982
1983
        /**
1984
         * removes the attachment
1985
         *
1986
         * This automatically hides the attachment containers to, to
1987
         * prevent an inconsistent display.
1988
         *
1989
         * @name AttachmentViewer.removeAttachment
1990
         * @function
1991
         */
1992
        me.removeAttachment = function()
1993
        {
1994
            me.hideAttachment();
1995
            me.hideAttachmentPreview();
1996
            $attachmentLink.prop('href', '');
1997
            $attachmentLink.prop('download', '');
1998
            $attachmentPreview.html('');
1999
        };
2000
2001
        /**
2002
         * hides the attachment
2003
         *
2004
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2005
         * for that) nor will it hide the attachment link if it was moved somewhere
2006
         * else (see AttachmentViewer.moveAttachmentTo).
2007
         *
2008
         * @name AttachmentViewer.hideAttachment
2009
         * @function
2010
         */
2011
        me.hideAttachment = function()
2012
        {
2013
            $attachment.addClass('hidden');
2014
        };
2015
2016
        /**
2017
         * hides the attachment preview
2018
         *
2019
         * @name AttachmentViewer.hideAttachmentPreview
2020
         * @function
2021
         */
2022
        me.hideAttachmentPreview = function()
2023
        {
2024
            $attachmentPreview.addClass('hidden');
2025
        };
2026
2027
        /**
2028
         * checks if there is an attachment
2029
         *
2030
         * @name   AttachmentViewer.hasAttachment
2031
         * @function
2032
         */
2033
        me.hasAttachment = function()
2034
        {
2035
            var link = $attachmentLink.prop('href');
2036
            return (typeof link !== 'undefined' && link !== '');
2037
        };
2038
2039
        /**
2040
         * return the attachment
2041
         *
2042
         * @name   AttachmentViewer.getAttachment
2043
         * @function
2044
         * @returns {array}
2045
         */
2046
        me.getAttachment = function()
2047
        {
2048
            return [
2049
                $attachmentLink.prop('href'),
2050
                $attachmentLink.prop('download')
2051
            ];
2052
        };
2053
2054
        /**
2055
         * moves the attachment link to another element
2056
         *
2057
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2058
         *
2059
         * @name   AttachmentViewer.moveAttachmentTo
2060
         * @function
2061
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2062
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2063
         */
2064
        me.moveAttachmentTo = function($element, label)
2065
        {
2066
            // move elemement to new place
2067
            $attachmentLink.appendTo($element);
2068
2069
            // update text
2070
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2071
        };
2072
2073
        /**
2074
         * initiate
2075
         *
2076
         * preloads jQuery elements
2077
         *
2078
         * @name   AttachmentViewer.init
2079
         * @function
2080
         */
2081
        me.init = function()
2082
        {
2083
            $attachment = $('#attachment');
2084
            $attachmentLink = $('#attachment a');
2085
            $attachmentPreview = $('#attachmentPreview');
2086
            attachmentHasPreview = false;
2087
        };
2088
2089
        return me;
2090
    })();
2091
2092
    /**
2093
     * (view) Shows discussion thread and handles replies
2094
     *
2095
     * @name   DiscussionViewer
2096
     * @class
2097
     */
2098
    var DiscussionViewer = (function () {
2099
        var me = {};
2100
2101
        var $commentTail,
2102
            $discussion,
2103
            $reply,
2104
            $replyMessage,
2105
            $replyNickname,
2106
            $replyStatus,
2107
            $commentContainer;
2108
2109
        var replyCommentId;
2110
2111
        /**
2112
         * initializes the templates
2113
         *
2114
         * @name   DiscussionViewer.initTemplates
2115
         * @private
2116
         * @function
2117
         */
2118
        function initTemplates()
2119
        {
2120
            $reply = Model.getTemplate('reply');
2121
            $replyMessage = $reply.find('#replymessage');
2122
            $replyNickname = $reply.find('#nickname');
2123
            $replyStatus = $reply.find('#replystatus');
2124
2125
            // cache jQuery elements
2126
            $commentTail = Model.getTemplate('commenttail');
2127
        }
2128
2129
        /**
2130
         * open the comment entry when clicking the "Reply" button of a comment
2131
         *
2132
         * @name   DiscussionViewer.openReply
2133
         * @private
2134
         * @function
2135
         * @param  {Event} event
2136
         */
2137
        function openReply(event)
2138
        {
2139
            var $source = $(event.target);
2140
2141
            // clear input
2142
            $replyMessage.val('');
2143
            $replyNickname.val('');
2144
2145
            // get comment id from source element
2146
            replyCommentId = $source.parent().prop('id').split('_')[1];
2147
2148
            // move to correct position
2149
            $source.after($reply);
2150
2151
            // show
2152
            $reply.removeClass('hidden');
2153
            $replyMessage.focus();
2154
2155
            event.preventDefault();
2156
        }
2157
2158
        /**
2159
         * custom handler for displaying notifications in own status message area
2160
         *
2161
         * @name   DiscussionViewer.handleNotification
2162
         * @function
2163
         * @param  {string} alertType
2164
         * @param  {jQuery} $element
2165
         * @param  {string|array} args
2166
         * @param  {string|null} icon
2167
         * @return {bool|jQuery}
2168
         */
2169
        me.handleNotification = function(alertType, $element, args, icon)
0 ignored issues
show
The parameter $element 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...
The parameter icon 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...
The parameter args 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...
2170
        {
2171
            // ignore loading messages
2172
            if (alertType === 'loading') {
2173
                return false;
2174
            }
2175
2176
            if (alertType === 'danger') {
2177
                $replyStatus.removeClass('alert-info');
2178
                $replyStatus.addClass('alert-danger');
2179
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2180
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2181
            } else {
2182
                $replyStatus.removeClass('alert-danger');
2183
                $replyStatus.addClass('alert-info');
2184
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2185
                $replyStatus.find(':first').addClass('glyphicon-alert');
2186
            }
2187
2188
            return $replyStatus;
2189
        };
2190
2191
        /**
2192
         * adds another comment
2193
         *
2194
         * @name   DiscussionViewer.addComment
2195
         * @function
2196
         * @param {object} comment
2197
         * @param {string} commentText
2198
         * @param {string} nickname
2199
         */
2200
        me.addComment = function(comment, commentText, nickname)
2201
        {
2202
            if (commentText === '') {
2203
                commentText = 'comment decryption failed';
2204
            }
2205
2206
            // create new comment based on template
2207
            var $commentEntry = Model.getTemplate('comment');
2208
            $commentEntry.prop('id', 'comment_' + comment.id);
2209
            var $commentEntryData = $commentEntry.find('div.commentdata');
2210
2211
            // set & parse text
2212
            $commentEntryData.html(
2213
                DOMPurify.sanitize(
2214
                    Helper.urls2links(commentText)
2215
                )
2216
            );
2217
2218
            // set nickname
2219
            if (nickname.length > 0) {
2220
                $commentEntry.find('span.nickname').text(nickname);
2221
            } else {
2222
                $commentEntry.find('span.nickname').html('<i></i>');
2223
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2224
            }
2225
2226
            // set date
2227
            $commentEntry.find('span.commentdate')
2228
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2229
                      .attr('title', 'CommentID: ' + comment.id);
2230
2231
            // if an avatar is available, display it
2232
            if (comment.meta.vizhash) {
2233
                $commentEntry.find('span.nickname')
2234
                             .before(
2235
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2236
                             );
2237
                $(document).on('languageLoaded', function () {
2238
                    $commentEntry.find('img.vizhash')
2239
                                 .prop('title', I18n._('Avatar generated from IP address'));
2240
                });
2241
            }
2242
2243
            // starting point (default value/fallback)
2244
            var $place = $commentContainer;
2245
2246
            // if parent comment exists
2247
            var $parentComment = $('#comment_' + comment.parentid);
2248
            if ($parentComment.length) {
2249
                // use parent as position for new comment, so it is shifted
2250
                // to the right
2251
                $place = $parentComment;
2252
            }
2253
2254
            // finally append comment
2255
            $place.append($commentEntry);
2256
        };
2257
2258
        /**
2259
         * finishes the discussion area after last comment
2260
         *
2261
         * @name   DiscussionViewer.finishDiscussion
2262
         * @function
2263
         */
2264
        me.finishDiscussion = function()
2265
        {
2266
            // add 'add new comment' area
2267
            $commentContainer.append($commentTail);
2268
2269
            // show discussions
2270
            $discussion.removeClass('hidden');
2271
        };
2272
2273
        /**
2274
         * removes the old discussion and prepares everything for creating a new
2275
         * one.
2276
         *
2277
         * @name   DiscussionViewer.prepareNewDiscussion
2278
         * @function
2279
         */
2280
        me.prepareNewDiscussion = function()
2281
        {
2282
            $commentContainer.html('');
2283
            $discussion.addClass('hidden');
2284
2285
            // (re-)init templates
2286
            initTemplates();
2287
        };
2288
2289
        /**
2290
         * returns the users message from the reply form
2291
         *
2292
         * @name   DiscussionViewer.getReplyMessage
2293
         * @function
2294
         * @return {String}
2295
         */
2296
        me.getReplyMessage = function()
2297
        {
2298
            return $replyMessage.val();
2299
        };
2300
2301
        /**
2302
         * returns the users nickname (if any) from the reply form
2303
         *
2304
         * @name   DiscussionViewer.getReplyNickname
2305
         * @function
2306
         * @return {String}
2307
         */
2308
        me.getReplyNickname = function()
2309
        {
2310
            return $replyNickname.val();
2311
        };
2312
2313
        /**
2314
         * returns the id of the parent comment the user is replying to
2315
         *
2316
         * @name   DiscussionViewer.getReplyCommentId
2317
         * @function
2318
         * @return {int|undefined}
2319
         */
2320
        me.getReplyCommentId = function()
2321
        {
2322
            return replyCommentId;
2323
        };
2324
2325
        /**
2326
         * highlights a specific comment and scrolls to it if necessary
2327
         *
2328
         * @name   DiscussionViewer.highlightComment
2329
         * @function
2330
         * @param {string} commentId
2331
         * @param {bool} fadeOut - whether to fade out the comment
2332
         */
2333
        me.highlightComment = function(commentId, fadeOut)
2334
        {
2335
            var $comment = $('#comment_' + commentId);
2336
            // in case comment does not exist, cancel
2337
            if ($comment.length === 0) {
2338
                return;
2339
            }
2340
2341
            var highlightComment = function () {
2342
                $comment.addClass('highlight');
2343
                if (fadeOut === true) {
2344
                    setTimeout(function () {
2345
                        $comment.removeClass('highlight');
2346
                    }, 300);
2347
                }
2348
            };
2349
2350
            if (UiHelper.isVisible($comment)) {
2351
                return highlightComment();
2352
            }
2353
2354
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2355
        };
2356
2357
        /**
2358
         * initiate
2359
         *
2360
         * preloads jQuery elements
2361
         *
2362
         * @name   DiscussionViewer.init
2363
         * @function
2364
         */
2365
        me.init = function()
2366
        {
2367
            // bind events to templates (so they are later cloned)
2368
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2369
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2370
2371
            $commentContainer = $('#commentcontainer');
2372
            $discussion = $('#discussion');
2373
        };
2374
2375
        return me;
2376
    })();
2377
2378
    /**
2379
     * Manage top (navigation) bar
2380
     *
2381
     * @name   TopNav
2382
     * @param  {object} window
2383
     * @param  {object} document
2384
     * @class
2385
     */
2386
    var TopNav = (function (window, document) {
2387
        var me = {};
2388
2389
        var createButtonsDisplayed = false;
2390
        var viewButtonsDisplayed = false;
2391
2392
        var $attach,
2393
            $burnAfterReading,
2394
            $burnAfterReadingOption,
2395
            $cloneButton,
2396
            $customAttachment,
2397
            $expiration,
2398
            $fileRemoveButton,
2399
            $fileWrap,
2400
            $formatter,
2401
            $newButton,
2402
            $openDiscussion,
2403
            $openDiscussionOption,
2404
            $password,
2405
            $passwordInput,
2406
            $rawTextButton,
2407
            $qrCodeLink,
2408
            $sendButton;
2409
2410
        var pasteExpiration = '1week';
2411
2412
        /**
2413
         * set the expiration on bootstrap templates in dropdown
2414
         *
2415
         * @name   TopNav.updateExpiration
2416
         * @private
2417
         * @function
2418
         * @param  {Event} event
2419
         */
2420
        function updateExpiration(event)
2421
        {
2422
            // get selected option
2423
            var target = $(event.target);
2424
2425
            // update dropdown display and save new expiration time
2426
            $('#pasteExpirationDisplay').text(target.text());
2427
            pasteExpiration = target.data('expiration');
2428
2429
            event.preventDefault();
2430
        }
2431
2432
        /**
2433
         * set the format on bootstrap templates in dropdown
2434
         *
2435
         * @name   TopNav.updateFormat
2436
         * @private
2437
         * @function
2438
         * @param  {Event} event
2439
         */
2440
        function updateFormat(event)
2441
        {
2442
            // get selected option
2443
            var $target = $(event.target);
2444
2445
            // update dropdown display and save new format
2446
            var newFormat = $target.data('format');
2447
            $('#pasteFormatterDisplay').text($target.text());
2448
            PasteViewer.setFormat(newFormat);
2449
2450
            // update preview
2451
            if (Editor.isPreview()) {
2452
                PasteViewer.run();
2453
            }
2454
2455
            event.preventDefault();
2456
        }
2457
2458
        /**
2459
         * when "burn after reading" is checked, disable discussion
2460
         *
2461
         * @name   TopNav.changeBurnAfterReading
2462
         * @private
2463
         * @function
2464
         */
2465
        function changeBurnAfterReading()
2466
        {
2467
            if ($burnAfterReading.is(':checked')) {
2468
                $openDiscussionOption.addClass('buttondisabled');
2469
                $openDiscussion.prop('checked', false);
2470
2471
                // if button is actually disabled, force-enable it and uncheck other button
2472
                $burnAfterReadingOption.removeClass('buttondisabled');
2473
            } else {
2474
                $openDiscussionOption.removeClass('buttondisabled');
2475
            }
2476
        }
2477
2478
        /**
2479
         * when discussion is checked, disable "burn after reading"
2480
         *
2481
         * @name   TopNav.changeOpenDiscussion
2482
         * @private
2483
         * @function
2484
         */
2485
        function changeOpenDiscussion()
2486
        {
2487
            if ($openDiscussion.is(':checked')) {
2488
                $burnAfterReadingOption.addClass('buttondisabled');
2489
                $burnAfterReading.prop('checked', false);
2490
2491
                // if button is actually disabled, force-enable it and uncheck other button
2492
                $openDiscussionOption.removeClass('buttondisabled');
2493
            } else {
2494
                $burnAfterReadingOption.removeClass('buttondisabled');
2495
            }
2496
        }
2497
2498
        /**
2499
         * return raw text
2500
         *
2501
         * @name   TopNav.rawText
2502
         * @private
2503
         * @function
2504
         * @param  {Event} event
2505
         */
2506
        function rawText(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...
2507
        {
2508
            TopNav.hideAllButtons();
2509
            Alert.showLoading('Showing raw text…', 0, 'time');
2510
            var paste = PasteViewer.getText();
2511
2512
            // push a new state to allow back navigation with browser back button
2513
            history.pushState(
2514
                {type: 'raw'},
2515
                document.title,
2516
                // recreate paste URL
2517
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2518
                Model.getPasteKey()
2519
            );
2520
2521
            // we use text/html instead of text/plain to avoid a bug when
2522
            // reloading the raw text view (it reverts to type text/html)
2523
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2524
            var newDoc = document.open('text/html', 'replace');
2525
            newDoc.write('<!DOCTYPE html><html><head>');
2526
            for (var i = 0; i < $head.length; i++) {
2527
                newDoc.write($head[i].outerHTML);
2528
            }
2529
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2530
            newDoc.close();
2531
        }
2532
2533
        /**
2534
         * saves the language in a cookie and reloads the page
2535
         *
2536
         * @name   TopNav.setLanguage
2537
         * @private
2538
         * @function
2539
         * @param  {Event} event
2540
         */
2541
        function setLanguage(event)
2542
        {
2543
            document.cookie = 'lang=' + $(event.target).data('lang');
2544
            UiHelper.reloadHome();
2545
        }
2546
2547
        /**
2548
         * hides all messages and creates a new paste
2549
         *
2550
         * @name   TopNav.clickNewPaste
2551
         * @private
2552
         * @function
2553
         * @param  {Event} event
2554
         */
2555
        function clickNewPaste(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...
2556
        {
2557
            Controller.hideStatusMessages();
2558
            Controller.newPaste();
2559
        }
2560
2561
        /**
2562
         * removes the existing attachment
2563
         *
2564
         * @name   TopNav.removeAttachment
2565
         * @private
2566
         * @function
2567
         * @param  {Event} event
2568
         */
2569
        function removeAttachment(event)
2570
        {
2571
            // if custom attachment is used, remove it first
2572
            if (!$customAttachment.hasClass('hidden')) {
2573
                AttachmentViewer.removeAttachment();
2574
                $customAttachment.addClass('hidden');
2575
                $fileWrap.removeClass('hidden');
2576
            }
2577
2578
            // our up-to-date jQuery can handle it :)
2579
            $fileWrap.find('input').val('');
2580
2581
            // pevent '#' from appearing in the URL
2582
            event.preventDefault();
2583
        }
2584
2585
        /**
2586
         * Shows the QR code of the current paste (URL).
2587
         *
2588
         * @name   TopNav.displayQrCode
2589
         * @function
2590
         * @param  {Event} event
2591
         */
2592
        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...
2593
        {
2594
            var qrCanvas = kjua({
2595
                render: 'canvas',
2596
                text: window.location.href
2597
            });
2598
            $('#qrcode-display').html(qrCanvas);
2599
        }
2600
2601
        /**
2602
         * Shows all elements belonging to viwing an existing pastes
2603
         *
2604
         * @name   TopNav.showViewButtons
2605
         * @function
2606
         */
2607
        me.showViewButtons = function()
2608
        {
2609
            if (viewButtonsDisplayed) {
2610
                console.warn('showViewButtons: view buttons are already displayed');
2611
                return;
2612
            }
2613
2614
            $newButton.removeClass('hidden');
2615
            $cloneButton.removeClass('hidden');
2616
            $rawTextButton.removeClass('hidden');
2617
            $qrCodeLink.removeClass('hidden');
2618
2619
            viewButtonsDisplayed = true;
2620
        };
2621
2622
        /**
2623
         * Hides all elements belonging to existing pastes
2624
         *
2625
         * @name   TopNav.hideViewButtons
2626
         * @function
2627
         */
2628
        me.hideViewButtons = function()
2629
        {
2630
            if (!viewButtonsDisplayed) {
2631
                console.warn('hideViewButtons: view buttons are already hidden');
2632
                return;
2633
            }
2634
2635
            $newButton.addClass('hidden');
2636
            $cloneButton.addClass('hidden');
2637
            $rawTextButton.addClass('hidden');
2638
            $qrCodeLink.addClass('hidden');
2639
2640
            viewButtonsDisplayed = false;
2641
        };
2642
2643
        /**
2644
         * Hides all elements belonging to existing pastes
2645
         *
2646
         * @name   TopNav.hideAllButtons
2647
         * @function
2648
         */
2649
        me.hideAllButtons = function()
2650
        {
2651
            me.hideViewButtons();
2652
            me.hideCreateButtons();
2653
        };
2654
2655
        /**
2656
         * shows all elements needed when creating a new paste
2657
         *
2658
         * @name   TopNav.showCreateButtons
2659
         * @function
2660
         */
2661
        me.showCreateButtons = function()
2662
        {
2663
            if (createButtonsDisplayed) {
2664
                console.warn('showCreateButtons: create buttons are already displayed');
2665
                return;
2666
            }
2667
2668
            $sendButton.removeClass('hidden');
2669
            $expiration.removeClass('hidden');
2670
            $formatter.removeClass('hidden');
2671
            $burnAfterReadingOption.removeClass('hidden');
2672
            $openDiscussionOption.removeClass('hidden');
2673
            $newButton.removeClass('hidden');
2674
            $password.removeClass('hidden');
2675
            $attach.removeClass('hidden');
2676
2677
            createButtonsDisplayed = true;
2678
        };
2679
2680
        /**
2681
         * shows all elements needed when creating a new paste
2682
         *
2683
         * @name   TopNav.hideCreateButtons
2684
         * @function
2685
         */
2686
        me.hideCreateButtons = function()
2687
        {
2688
            if (!createButtonsDisplayed) {
2689
                console.warn('hideCreateButtons: create buttons are already hidden');
2690
                return;
2691
            }
2692
2693
            $newButton.addClass('hidden');
2694
            $sendButton.addClass('hidden');
2695
            $expiration.addClass('hidden');
2696
            $formatter.addClass('hidden');
2697
            $burnAfterReadingOption.addClass('hidden');
2698
            $openDiscussionOption.addClass('hidden');
2699
            $password.addClass('hidden');
2700
            $attach.addClass('hidden');
2701
2702
            createButtonsDisplayed = false;
2703
        };
2704
2705
        /**
2706
         * only shows the "new paste" button
2707
         *
2708
         * @name   TopNav.showNewPasteButton
2709
         * @function
2710
         */
2711
        me.showNewPasteButton = function()
2712
        {
2713
            $newButton.removeClass('hidden');
2714
        };
2715
2716
        /**
2717
         * only hides the clone button
2718
         *
2719
         * @name   TopNav.hideCloneButton
2720
         * @function
2721
         */
2722
        me.hideCloneButton = function()
2723
        {
2724
            $cloneButton.addClass('hidden');
2725
        };
2726
2727
        /**
2728
         * only hides the raw text button
2729
         *
2730
         * @name   TopNav.hideRawButton
2731
         * @function
2732
         */
2733
        me.hideRawButton = function()
2734
        {
2735
            $rawTextButton.addClass('hidden');
2736
        };
2737
2738
        /**
2739
         * hides the file selector in attachment
2740
         *
2741
         * @name   TopNav.hideFileSelector
2742
         * @function
2743
         */
2744
        me.hideFileSelector = function()
2745
        {
2746
            $fileWrap.addClass('hidden');
2747
        };
2748
2749
2750
        /**
2751
         * shows the custom attachment
2752
         *
2753
         * @name   TopNav.showCustomAttachment
2754
         * @function
2755
         */
2756
        me.showCustomAttachment = function()
2757
        {
2758
            $customAttachment.removeClass('hidden');
2759
        };
2760
2761
        /**
2762
         * collapses the navigation bar if nedded
2763
         *
2764
         * @name   TopNav.collapseBar
2765
         * @function
2766
         */
2767
        me.collapseBar = function()
2768
        {
2769
            var $bar = $('.navbar-toggle');
2770
2771
            // check if bar is expanded
2772
            if ($bar.hasClass('collapse in')) {
2773
                // if so, toggle it
2774
                $bar.click();
2775
            }
2776
        };
2777
2778
        /**
2779
         * returns the currently set expiration time
2780
         *
2781
         * @name   TopNav.getExpiration
2782
         * @function
2783
         * @return {int}
2784
         */
2785
        me.getExpiration = function()
2786
        {
2787
            return pasteExpiration;
2788
        };
2789
2790
        /**
2791
         * returns the currently selected file(s)
2792
         *
2793
         * @name   TopNav.getFileList
2794
         * @function
2795
         * @return {FileList|null}
2796
         */
2797
        me.getFileList = function()
2798
        {
2799
            var $file = $('#file');
2800
2801
            // if no file given, return null
2802
            if (!$file.length || !$file[0].files.length) {
2803
                return null;
2804
            }
2805
            // @TODO is this really necessary
2806
            if (!($file[0].files && $file[0].files[0])) {
2807
                return null;
2808
            }
2809
2810
            return $file[0].files;
2811
        };
2812
2813
        /**
2814
         * returns the state of the burn after reading checkbox
2815
         *
2816
         * @name   TopNav.getExpiration
2817
         * @function
2818
         * @return {bool}
2819
         */
2820
        me.getBurnAfterReading = function()
2821
        {
2822
            return $burnAfterReading.is(':checked');
2823
        };
2824
2825
        /**
2826
         * returns the state of the discussion checkbox
2827
         *
2828
         * @name   TopNav.getOpenDiscussion
2829
         * @function
2830
         * @return {bool}
2831
         */
2832
        me.getOpenDiscussion = function()
2833
        {
2834
            return $openDiscussion.is(':checked');
2835
        };
2836
2837
        /**
2838
         * returns the entered password
2839
         *
2840
         * @name   TopNav.getPassword
2841
         * @function
2842
         * @return {string}
2843
         */
2844
        me.getPassword = function()
2845
        {
2846
            return $passwordInput.val();
2847
        };
2848
2849
        /**
2850
         * returns the element where custom attachments can be placed
2851
         *
2852
         * Used by AttachmentViewer when an attachment is cloned here.
2853
         *
2854
         * @name   TopNav.getCustomAttachment
2855
         * @function
2856
         * @return {jQuery}
2857
         */
2858
        me.getCustomAttachment = function()
2859
        {
2860
            return $customAttachment;
2861
        };
2862
2863
        /**
2864
         * init navigation manager
2865
         *
2866
         * preloads jQuery elements
2867
         *
2868
         * @name   TopNav.init
2869
         * @function
2870
         */
2871
        me.init = function()
2872
        {
2873
            $attach = $('#attach');
2874
            $burnAfterReading = $('#burnafterreading');
2875
            $burnAfterReadingOption = $('#burnafterreadingoption');
2876
            $cloneButton = $('#clonebutton');
2877
            $customAttachment = $('#customattachment');
2878
            $expiration = $('#expiration');
2879
            $fileRemoveButton = $('#fileremovebutton');
2880
            $fileWrap = $('#filewrap');
2881
            $formatter = $('#formatter');
2882
            $newButton = $('#newbutton');
2883
            $openDiscussion = $('#opendiscussion');
2884
            $openDiscussionOption = $('#opendiscussionoption');
2885
            $password = $('#password');
2886
            $passwordInput = $('#passwordinput');
2887
            $rawTextButton = $('#rawtextbutton');
2888
            $sendButton = $('#sendbutton');
2889
            $qrCodeLink = $('#qrcodelink');
2890
2891
            // bootstrap template drop down
2892
            $('#language ul.dropdown-menu li a').click(setLanguage);
2893
            // page template drop down
2894
            $('#language select option').click(setLanguage);
2895
2896
            // bind events
2897
            $burnAfterReading.change(changeBurnAfterReading);
2898
            $openDiscussionOption.change(changeOpenDiscussion);
2899
            $newButton.click(clickNewPaste);
2900
            $sendButton.click(PasteEncrypter.sendPaste);
2901
            $cloneButton.click(Controller.clonePaste);
2902
            $rawTextButton.click(rawText);
2903
            $fileRemoveButton.click(removeAttachment);
2904
            $qrCodeLink.click(displayQrCode);
2905
2906
            // bootstrap template drop downs
2907
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2908
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2909
2910
            // initiate default state of checkboxes
2911
            changeBurnAfterReading();
2912
            changeOpenDiscussion();
2913
2914
            // get default value from template or fall back to set value
2915
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2916
        };
2917
2918
        return me;
2919
    })(window, document);
2920
2921
    /**
2922
     * Responsible for AJAX requests, transparently handles encryption…
2923
     *
2924
     * @name   Uploader
2925
     * @class
2926
     */
2927
    var Uploader = (function () {
2928
        var me = {};
2929
2930
        var successFunc = null,
2931
            failureFunc = null,
2932
            url,
2933
            data,
2934
            symmetricKey,
2935
            password;
2936
2937
        /**
2938
         * public variable ('constant') for errors to prevent magic numbers
2939
         *
2940
         * @name   Uploader.error
2941
         * @readonly
2942
         * @enum   {Object}
2943
         */
2944
        me.error = {
2945
            okay: 0,
2946
            custom: 1,
2947
            unknown: 2,
2948
            serverError: 3
2949
        };
2950
2951
        /**
2952
         * ajaxHeaders to send in AJAX requests
2953
         *
2954
         * @name   Uploader.ajaxHeaders
2955
         * @private
2956
         * @readonly
2957
         * @enum   {Object}
2958
         */
2959
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2960
2961
        /**
2962
         * called after successful upload
2963
         *
2964
         * @name   Uploader.checkCryptParameters
2965
         * @private
2966
         * @function
2967
         * @throws {string}
2968
         */
2969
        function checkCryptParameters()
2970
        {
2971
            // workaround for this nasty 'bug' in ECMAScript
2972
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
2973
            var typeOfKey = typeof symmetricKey;
2974
            if (symmetricKey === null) {
2975
                typeOfKey = 'null';
2976
            }
2977
2978
            // in case of missing preparation, throw error
2979
            switch (typeOfKey) {
2980
                case 'string':
2981
                    // already set, all right
2982
                    return;
2983
                case 'null':
2984
                    // needs to be generated auto-generate
2985
                    symmetricKey = CryptTool.getSymmetricKey();
2986
                    break;
2987
                default:
2988
                    console.error('current invalid symmetricKey:', symmetricKey);
2989
                    throw 'symmetricKey is invalid, probably the module was not prepared';
2990
            }
2991
            // password is optional
2992
        }
2993
2994
        /**
2995
         * called after successful upload
2996
         *
2997
         * @name   Uploader.success
2998
         * @private
2999
         * @function
3000
         * @param {int} status
3001
         * @param {int} result - optional
3002
         */
3003
        function success(status, result)
3004
        {
3005
            // add useful data to result
3006
            result.encryptionKey = symmetricKey;
3007
            result.requestData = data;
3008
3009
            if (successFunc !== null) {
3010
                successFunc(status, result);
3011
            }
3012
        }
3013
3014
        /**
3015
         * called after a upload failure
3016
         *
3017
         * @name   Uploader.fail
3018
         * @private
3019
         * @function
3020
         * @param {int} status - internal code
3021
         * @param {int} result - original error code
3022
         */
3023
        function fail(status, result)
3024
        {
3025
            if (failureFunc !== null) {
3026
                failureFunc(status, result);
3027
            }
3028
        }
3029
3030
        /**
3031
         * actually uploads the data
3032
         *
3033
         * @name   Uploader.run
3034
         * @function
3035
         */
3036
        me.run = function()
3037
        {
3038
            $.ajax({
3039
                type: 'POST',
3040
                url: url,
3041
                data: data,
3042
                dataType: 'json',
3043
                headers: ajaxHeaders,
3044
                success: function(result) {
3045
                    if (result.status === 0) {
3046
                        success(0, result);
3047
                    } else if (result.status === 1) {
3048
                        fail(1, result);
3049
                    } else {
3050
                        fail(2, result);
3051
                    }
3052
                }
3053
            })
3054
            .fail(function(jqXHR, textStatus, errorThrown) {
3055
                console.error(textStatus, errorThrown);
3056
                fail(3, jqXHR);
3057
            });
3058
        };
3059
3060
        /**
3061
         * set success function
3062
         *
3063
         * @name   Uploader.setUrl
3064
         * @function
3065
         * @param {function} newUrl
3066
         */
3067
        me.setUrl = function(newUrl)
3068
        {
3069
            url = newUrl;
3070
        };
3071
3072
        /**
3073
         * sets the password to use (first value) and optionally also the
3074
         * encryption key (not recommend, it is automatically generated).
3075
         *
3076
         * Note: Call this after prepare() as prepare() resets these values.
3077
         *
3078
         * @name   Uploader.setCryptValues
3079
         * @function
3080
         * @param {string} newPassword
3081
         * @param {string} newKey       - optional
3082
         */
3083
        me.setCryptParameters = function(newPassword, newKey)
3084
        {
3085
            password = newPassword;
3086
3087
            if (typeof newKey !== 'undefined') {
3088
                symmetricKey = newKey;
3089
            }
3090
        };
3091
3092
        /**
3093
         * set success function
3094
         *
3095
         * @name   Uploader.setSuccess
3096
         * @function
3097
         * @param {function} func
3098
         */
3099
        me.setSuccess = function(func)
3100
        {
3101
            successFunc = func;
3102
        };
3103
3104
        /**
3105
         * set failure function
3106
         *
3107
         * @name   Uploader.setFailure
3108
         * @function
3109
         * @param {function} func
3110
         */
3111
        me.setFailure = function(func)
3112
        {
3113
            failureFunc = func;
3114
        };
3115
3116
        /**
3117
         * prepares a new upload
3118
         *
3119
         * Call this when doing a new upload to reset any data from potential
3120
         * previous uploads. Must be called before any other method of this
3121
         * module.
3122
         *
3123
         * @name   Uploader.prepare
3124
         * @function
3125
         * @return {object}
3126
         */
3127
        me.prepare = function()
3128
        {
3129
            // entropy should already be checked!
3130
3131
            // reset password
3132
            password = '';
3133
3134
            // reset key, so it a new one is generated when it is used
3135
            symmetricKey = null;
3136
3137
            // reset data
3138
            successFunc = null;
3139
            failureFunc = null;
3140
            url = Helper.baseUri();
3141
            data = {};
3142
        };
3143
3144
        /**
3145
         * encrypts and sets the data
3146
         *
3147
         * @name   Uploader.setData
3148
         * @function
3149
         * @param {string} index
3150
         * @param {mixed} element
3151
         */
3152
        me.setData = function(index, element)
3153
        {
3154
            checkCryptParameters();
3155
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3156
        };
3157
3158
        /**
3159
         * set the additional metadata to send unencrypted
3160
         *
3161
         * @name   Uploader.setUnencryptedData
3162
         * @function
3163
         * @param {string} index
3164
         * @param {mixed} element
3165
         */
3166
        me.setUnencryptedData = function(index, element)
3167
        {
3168
            data[index] = element;
3169
        };
3170
3171
        /**
3172
         * set the additional metadata to send unencrypted passed at once
3173
         *
3174
         * @name   Uploader.setUnencryptedData
3175
         * @function
3176
         * @param {object} newData
3177
         */
3178
        me.setUnencryptedBulkData = function(newData)
3179
        {
3180
            $.extend(data, newData);
3181
        };
3182
3183
        /**
3184
         * Helper, which parses shows a general error message based on the result of the Uploader
3185
         *
3186
         * @name    Uploader.parseUploadError
3187
         * @function
3188
         * @param {int} status
3189
         * @param {object} data
3190
         * @param {string} doThisThing - a human description of the action, which was tried
3191
         * @return {array}
3192
         */
3193
        me.parseUploadError = function(status, data, doThisThing) {
3194
            var errorArray;
3195
3196
            switch (status) {
3197
                case me.error.custom:
3198
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3199
                    break;
3200
                case me.error.unknown:
3201
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3202
                    break;
3203
                case me.error.serverError:
3204
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3205
                    break;
3206
                default:
3207
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3208
                    break;
3209
            }
3210
3211
            return errorArray;
3212
        };
3213
3214
        /**
3215
         * init Uploader
3216
         *
3217
         * @name   Uploader.init
3218
         * @function
3219
         */
3220
        me.init = function()
3221
        {
3222
            // nothing yet
3223
        };
3224
3225
        return me;
3226
    })();
3227
3228
    /**
3229
     * (controller) Responsible for encrypting paste and sending it to server.
3230
     *
3231
     * Does upload, encryption is done transparently by Uploader.
3232
     *
3233
     * @name PasteEncrypter
3234
     * @class
3235
     */
3236
    var PasteEncrypter = (function () {
3237
        var me = {};
3238
3239
        var requirementsChecked = false;
3240
3241
        /**
3242
         * checks whether there is a suitable amount of entrophy
3243
         *
3244
         * @name PasteEncrypter.checkRequirements
3245
         * @private
3246
         * @function
3247
         * @param {function} retryCallback - the callback to execute to retry the upload
3248
         * @return {bool}
3249
         */
3250
        function checkRequirements(retryCallback) {
3251
            // skip double requirement checks
3252
            if (requirementsChecked === true) {
3253
                return true;
3254
            }
3255
3256
            if (!CryptTool.isEntropyReady()) {
3257
                // display a message and wait
3258
                Alert.showStatus('Please move your mouse for more entropy…');
3259
3260
                CryptTool.addEntropySeedListener(retryCallback);
3261
                return false;
3262
            }
3263
3264
            requirementsChecked = true;
3265
3266
            return true;
3267
        }
3268
3269
        /**
3270
         * called after successful paste upload
3271
         *
3272
         * @name PasteEncrypter.showCreatedPaste
3273
         * @private
3274
         * @function
3275
         * @param {int} status
3276
         * @param {object} data
3277
         */
3278
        function showCreatedPaste(status, data) {
3279
            Alert.hideLoading();
3280
3281
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3282
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3283
3284
            Alert.hideMessages();
3285
3286
            // show notification
3287
            PasteStatus.createPasteNotification(url, deleteUrl);
3288
3289
            // show new URL in browser bar
3290
            history.pushState({type: 'newpaste'}, document.title, url);
3291
3292
            TopNav.showViewButtons();
3293
            TopNav.hideRawButton();
3294
            Editor.hide();
3295
3296
            // parse and show text
3297
            // (preparation already done in me.sendPaste())
3298
            PasteViewer.run();
3299
        }
3300
3301
        /**
3302
         * called after successful comment upload
3303
         *
3304
         * @name PasteEncrypter.showUploadedComment
3305
         * @private
3306
         * @function
3307
         * @param {int} status
3308
         * @param {object} data
3309
         */
3310
        function showUploadedComment(status, data) {
3311
            // show success message
3312
            // Alert.showStatus('Comment posted.');
3313
3314
            // reload paste
3315
            Controller.refreshPaste(function () {
3316
                // highlight sent comment
3317
                DiscussionViewer.highlightComment(data.id, true);
3318
                // reset error handler
3319
                Alert.setCustomHandler(null);
3320
            });
3321
        }
3322
3323
        /**
3324
         * adds attachments to the Uploader
3325
         *
3326
         * @name PasteEncrypter.encryptAttachments
3327
         * @private
3328
         * @function
3329
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3330
         * @param {function} callback - excuted when action is successful
3331
         */
3332
        function encryptAttachments(file, callback) {
3333
            if (typeof file !== 'undefined' && file !== null) {
3334
                // check file reader requirements for upload
3335
                if (typeof FileReader === 'undefined') {
3336
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3337
                    // cancels process as it does not execute callback
3338
                    return;
3339
                }
3340
3341
                var reader = new FileReader();
3342
3343
                // closure to capture the file information
3344
                reader.onload = function(event) {
3345
                    Uploader.setData('attachment', event.target.result);
3346
                    Uploader.setData('attachmentname', file.name);
3347
3348
                    // run callback
3349
                    return callback();
3350
                }
3351
3352
                // actually read first file
3353
                reader.readAsDataURL(file);
3354
            } else if (AttachmentViewer.hasAttachment()) {
3355
                // fall back to cloned part
3356
                var attachment = AttachmentViewer.getAttachment();
3357
3358
                Uploader.setData('attachment', attachment[0]);
3359
                Uploader.setData('attachmentname', attachment[1]);
3360
                return callback();
3361
            } else {
3362
                // if there are no attachments, this is of course still successful
3363
                return callback();
3364
            }
3365
        }
3366
3367
        /**
3368
         * send a reply in a discussion
3369
         *
3370
         * @name   PasteEncrypter.sendComment
3371
         * @function
3372
         */
3373
        me.sendComment = function()
3374
        {
3375
            Alert.hideMessages();
3376
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3377
3378
            // UI loading state
3379
            TopNav.hideAllButtons();
3380
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3381
3382
            // get data
3383
            var plainText = DiscussionViewer.getReplyMessage(),
3384
                nickname = DiscussionViewer.getReplyNickname(),
3385
                parentid = DiscussionViewer.getReplyCommentId();
3386
3387
            // do not send if there is no data
3388
            if (plainText.length === 0) {
3389
                // revert loading status…
3390
                Alert.hideLoading();
3391
                Alert.setCustomHandler(null);
3392
                TopNav.showViewButtons();
3393
                return;
3394
            }
3395
3396
            // check entropy
3397
            if (!checkRequirements(function () {
3398
                me.sendComment();
3399
            })) {
3400
                return; // to prevent multiple executions
3401
            }
3402
            Alert.showLoading(null, 10);
3403
3404
            // prepare Uploader
3405
            Uploader.prepare();
3406
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3407
3408
            // set success/fail functions
3409
            Uploader.setSuccess(showUploadedComment);
3410
            Uploader.setFailure(function (status, data) {
3411
                // revert loading status…
3412
                Alert.hideLoading();
3413
                TopNav.showViewButtons();
3414
3415
                // show error message
3416
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3417
3418
                // reset error handler
3419
                Alert.setCustomHandler(null);
3420
            });
3421
3422
            // fill it with unencrypted params
3423
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3424
            if (typeof parentid === 'undefined') {
3425
                // if parent id is not set, this is the top-most comment, so use
3426
                // paste id as parent @TODO is this really good?
3427
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3428
            } else {
3429
                Uploader.setUnencryptedData('parentid', parentid);
3430
            }
3431
3432
            // encrypt data
3433
            Uploader.setData('data', plainText);
3434
3435
            if (nickname.length > 0) {
3436
                Uploader.setData('nickname', nickname);
3437
            }
3438
3439
            Uploader.run();
3440
        };
3441
3442
        /**
3443
         * sends a new paste to server
3444
         *
3445
         * @name   PasteEncrypter.sendPaste
3446
         * @function
3447
         */
3448
        me.sendPaste = function()
3449
        {
3450
            // hide previous (error) messages
3451
            Controller.hideStatusMessages();
3452
3453
            // UI loading state
3454
            TopNav.hideAllButtons();
3455
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3456
            TopNav.collapseBar();
3457
3458
            // get data
3459
            var plainText = Editor.getText(),
3460
                format = PasteViewer.getFormat(),
3461
                files = TopNav.getFileList();
3462
3463
            // do not send if there is no data
3464
            if (plainText.length === 0 && files === null) {
3465
                // revert loading status…
3466
                Alert.hideLoading();
3467
                TopNav.showCreateButtons();
3468
                return;
3469
            }
3470
3471
            Alert.showLoading(null, 10);
3472
3473
            // check entropy
3474
            if (!checkRequirements(function () {
3475
                me.sendPaste();
3476
            })) {
3477
                return; // to prevent multiple executions
3478
            }
3479
3480
            // prepare Uploader
3481
            Uploader.prepare();
3482
            Uploader.setCryptParameters(TopNav.getPassword());
3483
3484
            // set success/fail functions
3485
            Uploader.setSuccess(showCreatedPaste);
3486
            Uploader.setFailure(function (status, data) {
3487
                // revert loading status…
3488
                Alert.hideLoading();
3489
                TopNav.showCreateButtons();
3490
3491
                // show error message
3492
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3493
            });
3494
3495
            // fill it with unencrypted submitted options
3496
            Uploader.setUnencryptedBulkData({
3497
                expire:           TopNav.getExpiration(),
3498
                formatter:        format,
3499
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3500
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3501
            });
3502
3503
            // prepare PasteViewer for later preview
3504
            PasteViewer.setText(plainText);
3505
            PasteViewer.setFormat(format);
3506
3507
            // encrypt cipher data
3508
            Uploader.setData('data', plainText);
3509
3510
            // encrypt attachments
3511
            encryptAttachments(
3512
                files === null ? null : files[0],
3513
                function () {
3514
                    // send data
3515
                    Uploader.run();
3516
                }
3517
            );
3518
        };
3519
3520
        /**
3521
         * initialize
3522
         *
3523
         * @name   PasteEncrypter.init
3524
         * @function
3525
         */
3526
        me.init = function()
3527
        {
3528
            // nothing yet
3529
        };
3530
3531
        return me;
3532
    })();
3533
3534
    /**
3535
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3536
     *
3537
     * Only decryption, no download.
3538
     *
3539
     * @name PasteDecrypter
3540
     * @class
3541
     */
3542
    var PasteDecrypter = (function () {
3543
        var me = {};
3544
3545
        /**
3546
         * decrypt data or prompts for password in cvase of failure
3547
         *
3548
         * @name   PasteDecrypter.decryptOrPromptPassword
3549
         * @private
3550
         * @function
3551
         * @param  {string} key
3552
         * @param  {string} password - optional, may be an empty string
3553
         * @param  {string} cipherdata
3554
         * @throws {string}
3555
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3556
         */
3557
        function decryptOrPromptPassword(key, password, cipherdata)
3558
        {
3559
            // try decryption without password
3560
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3561
3562
            // if it fails, request password
3563
            if (plaindata.length === 0 && password.length === 0) {
3564
                // try to get cached password first
3565
                password = Prompt.getPassword();
3566
3567
                // if password is there, re-try
3568
                if (password.length === 0) {
3569
                    password = Prompt.requestPassword();
3570
                }
3571
                // recursive
3572
                // note: an infinite loop is prevented as the previous if
3573
                // clause checks whether a password is already set and ignores
3574
                // errors when a password has been passed
3575
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3576
            }
3577
3578
            // if all tries failed, we can only return an error
3579
            if (plaindata.length === 0) {
3580
                throw 'failed to decipher data';
3581
            }
3582
3583
            return plaindata;
3584
        }
3585
3586
        /**
3587
         * decrypt the actual paste text
3588
         *
3589
         * @name   PasteDecrypter.decryptOrPromptPassword
3590
         * @private
3591
         * @function
3592
         * @param  {object} paste - paste data in object form
3593
         * @param  {string} key
3594
         * @param  {string} password
3595
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3596
         * @return {bool} whether action was successful
3597
         * @throws {string}
3598
         */
3599
        function decryptPaste(paste, key, password, ignoreError)
3600
        {
3601
            var plaintext;
3602
            if (ignoreError === true) {
3603
                plaintext = CryptTool.decipher(key, password, paste.data);
3604
            } else {
3605
                try {
3606
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3607
                } catch (err) {
3608
                    throw 'failed to decipher paste text: ' + err;
3609
                }
3610
                if (plaintext === false) {
3611
                    return false;
3612
                }
3613
            }
3614
3615
            // on success show paste
3616
            PasteViewer.setFormat(paste.meta.formatter);
3617
            PasteViewer.setText(plaintext);
3618
            // trigger to show the text (attachment loaded afterwards)
3619
            PasteViewer.run();
3620
3621
            return true;
3622
        }
3623
3624
        /**
3625
         * decrypts any attachment
3626
         *
3627
         * @name   PasteDecrypter.decryptAttachment
3628
         * @private
3629
         * @function
3630
         * @param  {object} paste - paste data in object form
3631
         * @param  {string} key
3632
         * @param  {string} password
3633
         * @return {bool} whether action was successful
3634
         * @throws {string}
3635
         */
3636
        function decryptAttachment(paste, key, password)
3637
        {
3638
            var attachment, attachmentName;
3639
3640
            // decrypt attachment
3641
            try {
3642
                attachment = decryptOrPromptPassword(key, password, paste.attachment);
3643
            } catch (err) {
3644
                throw 'failed to decipher attachment: ' + err;
3645
            }
3646
            if (attachment === false) {
3647
                return false;
3648
            }
3649
3650
            // decrypt attachment name
3651
            if (paste.attachmentname) {
3652
                try {
3653
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3654
                } catch (err) {
3655
                    throw 'failed to decipher attachment name: ' + err;
3656
                }
3657
                if (attachmentName === false) {
3658
                    return false;
3659
                }
3660
            }
3661
3662
            AttachmentViewer.setAttachment(attachment, attachmentName);
3663
            AttachmentViewer.showAttachment();
3664
3665
            return true;
3666
        }
3667
3668
        /**
3669
         * decrypts all comments and shows them
3670
         *
3671
         * @name   PasteDecrypter.decryptComments
3672
         * @private
3673
         * @function
3674
         * @param  {object} paste - paste data in object form
3675
         * @param  {string} key
3676
         * @param  {string} password
3677
         * @return {bool} whether action was successful
3678
         */
3679
        function decryptComments(paste, key, password)
3680
        {
3681
            // remove potentially previous discussion
3682
            DiscussionViewer.prepareNewDiscussion();
3683
3684
            // iterate over comments
3685
            for (var i = 0; i < paste.comments.length; ++i) {
3686
                var comment = paste.comments[i];
3687
3688
                DiscussionViewer.addComment(
3689
                    comment,
3690
                    CryptTool.decipher(key, password, comment.data),
3691
                    CryptTool.decipher(key, password, comment.meta.nickname)
3692
                );
3693
            }
3694
3695
            DiscussionViewer.finishDiscussion();
3696
            return true;
3697
        }
3698
3699
        /**
3700
         * show decrypted text in the display area, including discussion (if open)
3701
         *
3702
         * @name   PasteDecrypter.run
3703
         * @function
3704
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3705
         */
3706
        me.run = function(paste)
3707
        {
3708
            Alert.hideMessages();
3709
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3710
3711
            if (typeof paste === 'undefined') {
3712
                paste = $.parseJSON(Model.getCipherData());
3713
            }
3714
3715
            var key = Model.getPasteKey(),
3716
                password = Prompt.getPassword();
3717
3718
            if (PasteViewer.isPrettyPrinted()) {
3719
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3720
                return;
3721
            }
3722
3723
            // try to decrypt the paste
3724
            try {
3725
                // decrypt attachments
3726
                if (paste.attachment) {
3727
                    // try to decrypt paste and if it fails (because the password is
3728
                    // missing) return to let JS continue and wait for user
3729
                    if (!decryptAttachment(paste, key, password)) {
3730
                        return;
3731
                    }
3732
                    // ignore empty paste, as this is allowed when pasting attachments
3733
                    decryptPaste(paste, key, password, true);
3734
                } else {
3735
                    decryptPaste(paste, key, password);
3736
                }
3737
3738
3739
                // shows the remaining time (until) deletion
3740
                PasteStatus.showRemainingTime(paste.meta);
3741
3742
                // if the discussion is opened on this paste, display it
3743
                if (paste.meta.opendiscussion) {
3744
                    decryptComments(paste, key, password);
3745
                }
3746
3747
                Alert.hideLoading();
3748
                TopNav.showViewButtons();
3749
            } catch(err) {
3750
                Alert.hideLoading();
3751
3752
                // log and show error
3753
                console.error(err);
3754
                Alert.showError('Could not decrypt data (Wrong key?)');
3755
            }
3756
        };
3757
3758
        /**
3759
         * initialize
3760
         *
3761
         * @name   PasteDecrypter.init
3762
         * @function
3763
         */
3764
        me.init = function()
3765
        {
3766
            // nothing yet
3767
        };
3768
3769
        return me;
3770
    })();
3771
3772
    /**
3773
     * (controller) main PrivateBin logic
3774
     *
3775
     * @name   Controller
3776
     * @param  {object} window
3777
     * @param  {object} document
3778
     * @class
3779
     */
3780
    var Controller = (function (window, document) {
3781
        var me = {};
3782
3783
        /**
3784
         * hides all status messages no matter which module showed them
3785
         *
3786
         * @name   Controller.hideStatusMessages
3787
         * @function
3788
         */
3789
        me.hideStatusMessages = function()
3790
        {
3791
            PasteStatus.hideMessages();
3792
            Alert.hideMessages();
3793
        };
3794
3795
        /**
3796
         * creates a new paste
3797
         *
3798
         * @name   Controller.newPaste
3799
         * @function
3800
         */
3801
        me.newPaste = function()
3802
        {
3803
            // Important: This *must not* run Alert.hideMessages() as previous
3804
            // errors from viewing a paste should be shown.
3805
            TopNav.hideAllButtons();
3806
            Alert.showLoading('Preparing new paste…', 0, 'time');
3807
3808
            PasteStatus.hideMessages();
3809
            PasteViewer.hide();
3810
            Editor.resetInput();
3811
            Editor.show();
3812
            Editor.focusInput();
3813
3814
            TopNav.showCreateButtons();
3815
            Alert.hideLoading();
3816
        };
3817
3818
        /**
3819
         * shows the loaded paste
3820
         *
3821
         * @name   Controller.showPaste
3822
         * @function
3823
         */
3824
        me.showPaste = function()
3825
        {
3826
            try {
3827
                Model.getPasteId();
3828
                Model.getPasteKey();
3829
            } catch (err) {
3830
                console.error(err);
3831
3832
                // missing decryption key (or paste ID) in URL?
3833
                if (window.location.hash.length === 0) {
3834
                    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?)');
3835
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3836
                    return;
3837
                }
3838
            }
3839
3840
            // show proper elements on screen
3841
            PasteDecrypter.run();
3842
        };
3843
3844
        /**
3845
         * refreshes the loaded paste to show potential new data
3846
         *
3847
         * @name   Controller.refreshPaste
3848
         * @function
3849
         * @param  {function} callback
3850
         */
3851
        me.refreshPaste = function(callback)
3852
        {
3853
            // save window position to restore it later
3854
            var orgPosition = $(window).scrollTop();
3855
3856
            Uploader.prepare();
3857
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3858
3859
            Uploader.setFailure(function (status, data) {
3860
                // revert loading status…
3861
                Alert.hideLoading();
3862
                TopNav.showViewButtons();
3863
3864
                // show error message
3865
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3866
            });
3867
            Uploader.setSuccess(function (status, data) {
3868
                PasteDecrypter.run(data);
3869
3870
                // restore position
3871
                window.scrollTo(0, orgPosition);
3872
3873
                callback();
3874
            });
3875
            Uploader.run();
3876
        };
3877
3878
        /**
3879
         * clone the current paste
3880
         *
3881
         * @name   Controller.clonePaste
3882
         * @function
3883
         * @param  {Event} event
3884
         */
3885
        me.clonePaste = function(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...
3886
        {
3887
            TopNav.collapseBar();
3888
            TopNav.hideAllButtons();
3889
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3890
3891
            // hide messages from previous paste
3892
            me.hideStatusMessages();
3893
3894
            // erase the id and the key in url
3895
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3896
3897
            if (AttachmentViewer.hasAttachment()) {
3898
                AttachmentViewer.moveAttachmentTo(
3899
                    TopNav.getCustomAttachment(),
3900
                    'Cloned: \'%s\''
3901
                );
3902
                TopNav.hideFileSelector();
3903
                AttachmentViewer.hideAttachment();
3904
                // NOTE: it also looks nice without removing the attachment
3905
                // but for a consistent display we remove it…
3906
                AttachmentViewer.hideAttachmentPreview();
3907
                TopNav.showCustomAttachment();
3908
3909
                // show another status message to make the user aware that the
3910
                // file was cloned too!
3911
                Alert.showStatus(
3912
                    [
3913
                        'The cloned file \'%s\' was attached to this paste.',
3914
                        AttachmentViewer.getAttachment()[1]
3915
                    ], 'copy', true, true);
3916
            }
3917
3918
            Editor.setText(PasteViewer.getText());
3919
            PasteViewer.hide();
3920
            Editor.show();
3921
3922
            Alert.hideLoading();
3923
            TopNav.showCreateButtons();
3924
        };
3925
3926
        /**
3927
         * removes a saved paste
3928
         *
3929
         * @name   Controller.removePaste
3930
         * @function
3931
         * @param  {string} pasteId
3932
         * @param  {string} deleteToken
3933
         */
3934
        me.removePaste = function(pasteId, deleteToken) {
3935
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3936
            // so we use a POST request
3937
            Uploader.prepare();
3938
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3939
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3940
3941
            Uploader.setFailure(function () {
3942
                Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3943
            })
3944
            Uploader.run();
3945
        };
3946
3947
        /**
3948
         * application start
3949
         *
3950
         * @name   Controller.init
3951
         * @function
3952
         */
3953
        me.init = function()
3954
        {
3955
            // first load translations
3956
            I18n.loadTranslations();
3957
3958
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
3959
3960
            // initialize other modules/"classes"
3961
            Alert.init();
3962
            Model.init();
3963
            AttachmentViewer.init();
3964
            DiscussionViewer.init();
3965
            Editor.init();
3966
            PasteDecrypter.init();
3967
            PasteEncrypter.init();
3968
            PasteStatus.init();
3969
            PasteViewer.init();
3970
            Prompt.init();
3971
            TopNav.init();
3972
            UiHelper.init();
3973
            Uploader.init();
3974
3975
            // display an existing paste
3976
            if (Model.hasCipherData()) {
3977
                return me.showPaste();
3978
            }
3979
3980
            // otherwise create a new paste
3981
            me.newPaste();
3982
        };
3983
3984
        return me;
3985
    })(window, document);
3986
3987
    return {
3988
        Helper: Helper,
3989
        I18n: I18n,
3990
        CryptTool: CryptTool,
3991
        Model: Model,
3992
        UiHelper: UiHelper,
3993
        Alert: Alert,
3994
        PasteStatus: PasteStatus,
3995
        Prompt: Prompt,
3996
        Editor: Editor,
3997
        PasteViewer: PasteViewer,
3998
        AttachmentViewer: AttachmentViewer,
3999
        DiscussionViewer: DiscussionViewer,
4000
        TopNav: TopNav,
4001
        Uploader: Uploader,
4002
        PasteEncrypter: PasteEncrypter,
4003
        PasteDecrypter: PasteDecrypter,
4004
        Controller: Controller
4005
    };
4006
})(jQuery, sjcl, Base64, RawDeflate);
4007