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

js/privatebin.js (8 issues)

Code
1
/**
2
 * PrivateBin
3
 *
4
 * a zero-knowledge paste bin
5
 *
6
 * @see       {@link https://github.com/PrivateBin/PrivateBin}
7
 * @copyright 2012 Sébastien SAUVAGE ({@link http://sebsauvage.net})
8
 * @license   {@link https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
9
 * @version   1.1.1
10
 * @name      PrivateBin
11
 * @namespace
12
 */
13
14
/** global: Base64 */
15
/** global: DOMPurify */
16
/** global: FileReader */
17
/** global: RawDeflate */
18
/** global: history */
19
/** global: navigator */
20
/** global: prettyPrint */
21
/** global: prettyPrintOne */
22
/** global: showdown */
23
/** global: sjcl */
24
/** global: kjua */
25
26
// Immediately start random number generator collector.
27
sjcl.random.startCollectors();
28
29
// main application start, called when DOM is fully loaded
30
jQuery(document).ready(function() {
31
    // run main controller
32
    $.PrivateBin.Controller.init();
33
});
34
35
jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
36
    'use strict';
37
38
    /**
39
     * static Helper methods
40
     *
41
     * @name Helper
42
     * @class
43
     */
44
    var Helper = (function () {
45
        var me = {};
46
47
        /**
48
         * cache for script location
49
         *
50
         * @name Helper.baseUri
51
         * @private
52
         * @enum   {string|null}
53
         */
54
        var baseUri = null;
55
56
        /**
57
         * converts a duration (in seconds) into human friendly approximation
58
         *
59
         * @name Helper.secondsToHuman
60
         * @function
61
         * @param  {number} seconds
62
         * @return {Array}
63
         */
64
        me.secondsToHuman = function(seconds)
65
        {
66
            var v;
67
            if (seconds < 60)
68
            {
69
                v = Math.floor(seconds);
70
                return [v, 'second'];
71
            }
72
            if (seconds < 60 * 60)
73
            {
74
                v = Math.floor(seconds / 60);
75
                return [v, 'minute'];
76
            }
77
            if (seconds < 60 * 60 * 24)
78
            {
79
                v = Math.floor(seconds / (60 * 60));
80
                return [v, 'hour'];
81
            }
82
            // If less than 2 months, display in days:
83
            if (seconds < 60 * 60 * 24 * 60)
84
            {
85
                v = Math.floor(seconds / (60 * 60 * 24));
86
                return [v, 'day'];
87
            }
88
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
89
            return [v, 'month'];
90
        }
91
92
        /**
93
         * text range selection
94
         *
95
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
96
         * @name   Helper.selectText
97
         * @function
98
         * @param  {HTMLElement} element
99
         */
100
        me.selectText = function(element)
101
        {
102
            var range, selection;
103
104
            // MS
105
            if (document.body.createTextRange) {
106
                range = document.body.createTextRange();
107
                range.moveToElementText(element);
108
                range.select();
109
            } else if (window.getSelection) {
110
                selection = window.getSelection();
111
                range = document.createRange();
112
                range.selectNodeContents(element);
113
                selection.removeAllRanges();
114
                selection.addRange(range);
115
            }
116
        }
117
118
        /**
119
         * convert URLs to clickable links.
120
         * URLs to handle:
121
         * <pre>
122
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
123
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
124
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
125
         * </pre>
126
         *
127
         * @name   Helper.urls2links
128
         * @function
129
         * @param  {string} html
130
         * @return {string}
131
         */
132
        me.urls2links = function(html)
133
        {
134
            return html.replace(
135
                /(((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig,
136
                '<a href="$1" rel="nofollow">$1</a>'
137
            );
138
        }
139
140
        /**
141
         * minimal sprintf emulation for %s and %d formats
142
         *
143
         * Note that this function needs the parameters in the same order as the
144
         * format strings appear in the string, contrary to the original.
145
         *
146
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
147
         * @name   Helper.sprintf
148
         * @function
149
         * @param  {string} format
0 ignored issues
show
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
150
         * @param  {...*} args - one or multiple parameters injected into format string
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
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
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
291
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
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
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
313
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
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}));
0 ignored issues
show
The variable PopStateEvent seems to be never declared. If this is a global, consider adding a /** global: PopStateEvent */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
908
        }
909
910
        /**
911
         * initialize
912
         *
913
         * @name   UiHelper.init
914
         * @function
915
         */
916
        me.init = function()
917
        {
918
            // update link to home page
919
            $('.reloadlink').prop('href', Helper.baseUri());
920
921
            $(window).on('popstate', historyChange);
922
        }
923
924
        return me;
925
    })();
926
927
    /**
928
     * Alert/error manager
929
     *
930
     * @name   Alert
931
     * @class
932
     */
933
    var Alert = (function () {
934
        var me = {};
935
936
        var $errorMessage,
937
            $loadingIndicator,
938
            $statusMessage,
939
            $remainingTime;
940
941
        var currentIcon;
942
943
        var alertType = [
944
            'loading', // not in bootstrap, but using a good value here
945
            'info', // status icon
946
            'warning', // not used yet
947
            'danger' // error icon
948
        ];
949
950
        var customHandler;
951
952
        /**
953
         * forwards a request to the i18n module and shows the element
954
         *
955
         * @name   Alert.handleNotification
956
         * @private
957
         * @function
958
         * @param  {int} id - id of notification
959
         * @param  {jQuery} $element - jQuery object
960
         * @param  {string|array} args
961
         * @param  {string|null} icon - optional, icon
962
         */
963
        function handleNotification(id, $element, args, icon)
964
        {
965
            // basic parsing/conversion of parameters
966
            if (typeof icon === 'undefined') {
967
                icon = null;
968
            }
969
            if (typeof args === 'undefined') {
970
                args = null;
971
            } else if (typeof args === 'string') {
972
                // convert string to array if needed
973
                args = [args];
974
            }
975
976
            // pass to custom handler if defined
977
            if (typeof customHandler === 'function') {
978
                var handlerResult = customHandler(alertType[id], $element, args, icon);
979
                if (handlerResult === true) {
980
                    // if it returs true, skip own handler
981
                    return;
982
                }
983
                if (handlerResult instanceof jQuery) {
984
                    // continue processing with new element
985
                    $element = handlerResult;
986
                    icon = null; // icons not supported in this case
987
                }
988
            }
989
990
            // handle icon
991
            if (icon !== null && // icon was passed
992
                icon !== currentIcon[id] // and it differs from current icon
993
            ) {
994
                var $glyphIcon = $element.find(':first');
995
996
                // remove (previous) icon
997
                $glyphIcon.removeClass(currentIcon[id]);
998
999
                // any other thing as a string (e.g. 'null') (only) removes the icon
1000
                if (typeof icon === 'string') {
1001
                    // set new icon
1002
                    currentIcon[id] = 'glyphicon-' + icon;
1003
                    $glyphIcon.addClass(currentIcon[id]);
1004
                }
1005
            }
1006
1007
            // show text
1008
            if (args !== null) {
1009
                // add jQuery object to it as first parameter
1010
                args.unshift($element);
1011
                // pass it to I18n
1012
                I18n._.apply(this, args);
1013
            }
1014
1015
            // show notification
1016
            $element.removeClass('hidden');
1017
        }
1018
1019
        /**
1020
         * display a status message
1021
         *
1022
         * This automatically passes the text to I18n for translation.
1023
         *
1024
         * @name   Alert.showStatus
1025
         * @function
1026
         * @param  {string|array} message     string, use an array for %s/%d options
1027
         * @param  {string|null}  icon        optional, the icon to show,
1028
         *                                    default: leave previous icon
1029
         * @param  {bool}         dismissable optional, whether the notification
1030
         *                                    can be dismissed (closed), default: false
1031
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1032
         *                                    notification should be hidden automatically;
1033
         *                                    default: disabled (0); use true for default value
1034
         */
1035
        me.showStatus = function(message, icon, dismissable, autoclose)
1036
        {
1037
            console.info('status shown: ', message);
1038
            // @TODO: implement dismissable
1039
            // @TODO: implement autoclose
1040
1041
            handleNotification(1, $statusMessage, message, icon);
1042
        }
1043
1044
        /**
1045
         * display an error message
1046
         *
1047
         * This automatically passes the text to I18n for translation.
1048
         *
1049
         * @name   Alert.showError
1050
         * @function
1051
         * @param  {string|array} message     string, use an array for %s/%d options
1052
         * @param  {string|null}  icon        optional, the icon to show, default:
1053
         *                                    leave previous icon
1054
         * @param  {bool}         dismissable optional, whether the notification
1055
         *                                    can be dismissed (closed), default: false
1056
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1057
         *                                    notification should be hidden automatically;
1058
         *                                    default: disabled (0); use true for default value
1059
         */
1060
        me.showError = function(message, icon, dismissable, autoclose)
1061
        {
1062
            console.error('error message shown: ', message);
1063
            // @TODO: implement dismissable (bootstrap add-on has it)
1064
            // @TODO: implement autoclose
1065
1066
            handleNotification(3, $errorMessage, message, icon);
1067
        }
1068
1069
        /**
1070
         * display remaining message
1071
         *
1072
         * This automatically passes the text to I18n for translation.
1073
         *
1074
         * @name   Alert.showRemaining
1075
         * @function
1076
         * @param  {string|array} message     string, use an array for %s/%d options
1077
         */
1078
        me.showRemaining = function(message)
1079
        {
1080
            console.info('remaining message shown: ', message);
1081
            handleNotification(1, $remainingTime, message);
1082
        }
1083
1084
        /**
1085
         * shows a loading message, optionally with a percentage
1086
         *
1087
         * This automatically passes all texts to the i10s module.
1088
         *
1089
         * @name   Alert.showLoading
1090
         * @function
1091
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1092
         * @param  {int}               percentage   optional, default: null
1093
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1094
         */
1095
        me.showLoading = function(message, percentage, icon)
1096
        {
1097
            if (typeof message !== 'undefined' && message !== null) {
1098
                console.info('status changed: ', message);
1099
            }
1100
1101
            // default message text
1102
            if (typeof message === 'undefined') {
1103
                message = 'Loading…';
1104
            }
1105
1106
            // currently percentage parameter is ignored
1107
            // // @TODO handle it here…
1108
1109
            handleNotification(0, $loadingIndicator, message, icon);
1110
1111
            // show loading status (cursor)
1112
            $('body').addClass('loading');
1113
        }
1114
1115
        /**
1116
         * hides the loading message
1117
         *
1118
         * @name   Alert.hideLoading
1119
         * @function
1120
         */
1121
        me.hideLoading = function()
1122
        {
1123
            $loadingIndicator.addClass('hidden');
1124
1125
            // hide loading cursor
1126
            $('body').removeClass('loading');
1127
        }
1128
1129
        /**
1130
         * hides any status/error messages
1131
         *
1132
         * This does not include the loading message.
1133
         *
1134
         * @name   Alert.hideMessages
1135
         * @function
1136
         */
1137
        me.hideMessages = function()
1138
        {
1139
            // also possible: $('.statusmessage').addClass('hidden');
1140
            $statusMessage.addClass('hidden');
1141
            $errorMessage.addClass('hidden');
1142
        }
1143
1144
        /**
1145
         * set a custom handler, which gets all notifications.
1146
         *
1147
         * This handler gets the following arguments:
1148
         * alertType (see array), $element, args, icon
1149
         * If it returns true, the own processing will be stopped so the message
1150
         * will not be displayed. Otherwise it will continue.
1151
         * As an aditional feature it can return q jQuery element, which will
1152
         * then be used to add the message there. Icons are not supported in
1153
         * that case and will be ignored.
1154
         * Pass 'null' to reset/delete the custom handler.
1155
         * Note that there is no notification when a message is supposed to get
1156
         * hidden.
1157
         *
1158
         * @name   Alert.setCustomHandler
1159
         * @function
1160
         * @param {function|null} newHandler
1161
         */
1162
        me.setCustomHandler = function(newHandler)
1163
        {
1164
            customHandler = newHandler;
1165
        }
1166
1167
        /**
1168
         * init status manager
1169
         *
1170
         * preloads jQuery elements
1171
         *
1172
         * @name   Alert.init
1173
         * @function
1174
         */
1175
        me.init = function()
1176
        {
1177
            // hide "no javascript" error message
1178
            $('#noscript').hide();
1179
1180
            // not a reset, but first set of the elements
1181
            $errorMessage = $('#errormessage');
1182
            $loadingIndicator = $('#loadingindicator');
1183
            $statusMessage = $('#status');
1184
            $remainingTime = $('#remainingtime');
1185
1186
            currentIcon = [
1187
                'glyphicon-time', // loading icon
1188
                'glyphicon-info-sign', // status icon
1189
                '', // reserved for warning, not used yet
1190
                'glyphicon-alert' // error icon
1191
            ];
1192
        }
1193
1194
        return me;
1195
    })();
1196
1197
    /**
1198
     * handles paste status/result
1199
     *
1200
     * @name   PasteStatus
1201
     * @class
1202
     */
1203
    var PasteStatus = (function () {
1204
        var me = {};
1205
1206
        var $pasteSuccess,
1207
            $pasteUrl,
1208
            $remainingTime,
1209
            $shortenButton;
1210
1211
        /**
1212
         * forward to URL shortener
1213
         *
1214
         * @name   PasteStatus.sendToShortener
1215
         * @private
1216
         * @function
1217
         * @param  {Event} event
1218
         */
1219
        function sendToShortener(event)
1220
        {
1221
            window.location.href = $shortenButton.data('shortener')
1222
                                   + encodeURIComponent($pasteUrl.attr('href'));
1223
        }
1224
1225
        /**
1226
         * Forces opening the paste if the link does not do this automatically.
1227
         *
1228
         * This is necessary as browsers will not reload the page when it is
1229
         * already loaded (which is fake as it is set via history.pushState()).
1230
         *
1231
         * @name   PasteStatus.pasteLinkClick
1232
         * @function
1233
         * @param  {Event} event
1234
         */
1235
        function pasteLinkClick(event)
1236
        {
1237
            // check if location is (already) shown in URL bar
1238
            if (window.location.href === $pasteUrl.attr('href')) {
1239
                // if so we need to load link by reloading the current site
1240
                window.location.reload(true);
1241
            }
1242
        }
1243
1244
        /**
1245
         * creates a notification after a successfull paste upload
1246
         *
1247
         * @name   PasteStatus.createPasteNotification
1248
         * @function
1249
         * @param  {string} url
1250
         * @param  {string} deleteUrl
1251
         */
1252
        me.createPasteNotification = function(url, deleteUrl)
1253
        {
1254
            $('#pastelink').html(
1255
                I18n._(
1256
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1257
                    url, url
1258
                )
1259
            );
1260
            // save newly created element
1261
            $pasteUrl = $('#pasteurl');
1262
            // and add click event
1263
            $pasteUrl.click(pasteLinkClick);
1264
1265
            // shorten button
1266
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1267
1268
            // show result
1269
            $pasteSuccess.removeClass('hidden');
1270
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1271
            Helper.selectText($pasteUrl[0]);
1272
        }
1273
1274
        /**
1275
         * shows the remaining time
1276
         *
1277
         * @name PasteStatus.showRemainingTime
1278
         * @function
1279
         * @param {object} pasteMetaData
1280
         */
1281
        me.showRemainingTime = function(pasteMetaData)
1282
        {
1283
            if (pasteMetaData.burnafterreading) {
1284
                // display paste "for your eyes only" if it is deleted
1285
1286
                // actually remove paste, before we claim it is deleted
1287
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1288
1289
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1290
                $remainingTime.addClass('foryoureyesonly');
1291
1292
                // discourage cloning (it cannot really be prevented)
1293
                TopNav.hideCloneButton();
1294
1295
            } else if (pasteMetaData.expire_date) {
1296
                // display paste expiration
1297
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1298
                    expirationLabel = [
1299
                        'This document will expire in %d ' + expiration[1] + '.',
1300
                        'This document will expire in %d ' + expiration[1] + 's.'
1301
                    ];
1302
1303
                Alert.showRemaining([expirationLabel, expiration[0]]);
1304
                $remainingTime.removeClass('foryoureyesonly');
1305
            } else {
1306
                // never expires
1307
                return;
1308
            }
1309
1310
            // in the end, display notification
1311
            $remainingTime.removeClass('hidden');
1312
        }
1313
1314
        /**
1315
         * hides the remaining time and successful upload notification
1316
         *
1317
         * @name PasteStatus.hideRemainingTime
1318
         * @function
1319
         */
1320
        me.hideMessages = function()
1321
        {
1322
            $remainingTime.addClass('hidden');
1323
            $pasteSuccess.addClass('hidden');
1324
        }
1325
1326
        /**
1327
         * init status manager
1328
         *
1329
         * preloads jQuery elements
1330
         *
1331
         * @name   PasteStatus.init
1332
         * @function
1333
         */
1334
        me.init = function()
1335
        {
1336
            $pasteSuccess = $('#pastesuccess');
1337
            // $pasteUrl is saved in me.createPasteNotification() after creation
1338
            $remainingTime = $('#remainingtime');
1339
            $shortenButton = $('#shortenbutton');
1340
1341
            // bind elements
1342
            $shortenButton.click(sendToShortener);
1343
        }
1344
1345
        return me;
1346
    })();
1347
1348
    /**
1349
     * password prompt
1350
     *
1351
     * @name Prompt
1352
     * @class
1353
     */
1354
    var Prompt = (function () {
1355
        var me = {};
1356
1357
        var $passwordDecrypt,
1358
            $passwordForm,
1359
            $passwordModal;
1360
1361
        var password = '';
1362
1363
        /**
1364
         * submit a password in the modal dialog
1365
         *
1366
         * @name Prompt.submitPasswordModal
1367
         * @private
1368
         * @function
1369
         * @param  {Event} event
1370
         */
1371
        function submitPasswordModal(event)
1372
        {
1373
            event.preventDefault();
1374
1375
            // get input
1376
            password = $passwordDecrypt.val();
1377
1378
            // hide modal
1379
            $passwordModal.modal('hide');
1380
1381
            PasteDecrypter.run();
1382
        }
1383
1384
        /**
1385
         * ask the user for the password and set it
1386
         *
1387
         * @name Prompt.requestPassword
1388
         * @function
1389
         */
1390
        me.requestPassword = function()
1391
        {
1392
            // show new bootstrap method (if available)
1393
            if ($passwordModal.length !== 0) {
1394
                $passwordModal.modal({
1395
                    backdrop: 'static',
1396
                    keyboard: false
1397
                });
1398
                return;
1399
            }
1400
1401
            // fallback to old method for page template
1402
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1403
            if (newPassword === null) {
1404
                throw 'password prompt canceled';
1405
            }
1406
            if (password.length === 0) {
1407
                // recurse…
1408
                return me.requestPassword();
1409
            }
1410
1411
            password = newPassword;
1412
        }
1413
1414
        /**
1415
         * get the cached password
1416
         *
1417
         * If you do not get a password with this function
1418
         * (returns an empty string), use requestPassword.
1419
         *
1420
         * @name   Prompt.getPassword
1421
         * @function
1422
         * @return {string}
1423
         */
1424
        me.getPassword = function()
1425
        {
1426
            return password;
1427
        }
1428
1429
        /**
1430
         * init status manager
1431
         *
1432
         * preloads jQuery elements
1433
         *
1434
         * @name   Prompt.init
1435
         * @function
1436
         */
1437
        me.init = function()
1438
        {
1439
            $passwordDecrypt = $('#passworddecrypt');
1440
            $passwordForm = $('#passwordform');
1441
            $passwordModal = $('#passwordmodal');
1442
1443
            // bind events
1444
1445
            // focus password input when it is shown
1446
            $passwordModal.on('shown.bs.Model', function () {
1447
                $passwordDecrypt.focus();
1448
            });
1449
            // handle Model password submission
1450
            $passwordForm.submit(submitPasswordModal);
1451
        }
1452
1453
        return me;
1454
    })();
1455
1456
    /**
1457
     * Manage paste/message input, and preview tab
1458
     *
1459
     * Note that the actual preview is handled by PasteViewer.
1460
     *
1461
     * @name   Editor
1462
     * @class
1463
     */
1464
    var Editor = (function () {
1465
        var me = {};
1466
1467
        var $editorTabs,
1468
            $messageEdit,
1469
            $messagePreview,
1470
            $message;
1471
1472
        var isPreview = false;
1473
1474
        /**
1475
         * support input of tab character
1476
         *
1477
         * @name   Editor.supportTabs
1478
         * @function
1479
         * @param  {Event} event
1480
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1481
         */
1482
        function supportTabs(event)
1483
        {
1484
            var keyCode = event.keyCode || event.which;
1485
            // tab was pressed
1486
            if (keyCode === 9) {
1487
                // get caret position & selection
1488
                var val   = this.value,
1489
                    start = this.selectionStart,
1490
                    end   = this.selectionEnd;
1491
                // set textarea value to: text before caret + tab + text after caret
1492
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1493
                // put caret at right position again
1494
                this.selectionStart = this.selectionEnd = start + 1;
1495
                // prevent the textarea to lose focus
1496
                event.preventDefault();
1497
            }
1498
        }
1499
1500
        /**
1501
         * view the Editor tab
1502
         *
1503
         * @name   Editor.viewEditor
1504
         * @function
1505
         * @param  {Event} event - optional
1506
         */
1507
        function viewEditor(event)
1508
        {
1509
            // toggle buttons
1510
            $messageEdit.addClass('active');
1511
            $messagePreview.removeClass('active');
1512
1513
            PasteViewer.hide();
1514
1515
            // reshow input
1516
            $message.removeClass('hidden');
1517
1518
            me.focusInput();
1519
1520
            // finish
1521
            isPreview = false;
1522
1523
            // prevent jumping of page to top
1524
            if (typeof event !== 'undefined') {
1525
                event.preventDefault();
1526
            }
1527
        }
1528
1529
        /**
1530
         * view the preview tab
1531
         *
1532
         * @name   Editor.viewPreview
1533
         * @function
1534
         * @param  {Event} event
1535
         */
1536
        function viewPreview(event)
1537
        {
1538
            // toggle buttons
1539
            $messageEdit.removeClass('active');
1540
            $messagePreview.addClass('active');
1541
1542
            // hide input as now preview is shown
1543
            $message.addClass('hidden');
1544
1545
            // show preview
1546
            PasteViewer.setText($message.val());
1547
            PasteViewer.run();
1548
1549
            // finish
1550
            isPreview = true;
1551
1552
            // prevent jumping of page to top
1553
            if (typeof event !== 'undefined') {
1554
                event.preventDefault();
1555
            }
1556
        }
1557
1558
        /**
1559
         * get the state of the preview
1560
         *
1561
         * @name   Editor.isPreview
1562
         * @function
1563
         */
1564
        me.isPreview = function()
1565
        {
1566
            return isPreview;
1567
        }
1568
1569
        /**
1570
         * reset the Editor view
1571
         *
1572
         * @name   Editor.resetInput
1573
         * @function
1574
         */
1575
        me.resetInput = function()
1576
        {
1577
            // go back to input
1578
            if (isPreview) {
1579
                viewEditor();
1580
            }
1581
1582
            // clear content
1583
            $message.val('');
1584
        }
1585
1586
        /**
1587
         * shows the Editor
1588
         *
1589
         * @name   Editor.show
1590
         * @function
1591
         */
1592
        me.show = function()
1593
        {
1594
            $message.removeClass('hidden');
1595
            $editorTabs.removeClass('hidden');
1596
        }
1597
1598
        /**
1599
         * hides the Editor
1600
         *
1601
         * @name   Editor.reset
1602
         * @function
1603
         */
1604
        me.hide = function()
1605
        {
1606
            $message.addClass('hidden');
1607
            $editorTabs.addClass('hidden');
1608
        }
1609
1610
        /**
1611
         * focuses the message input
1612
         *
1613
         * @name   Editor.focusInput
1614
         * @function
1615
         */
1616
        me.focusInput = function()
1617
        {
1618
            $message.focus();
1619
        }
1620
1621
        /**
1622
         * sets a new text
1623
         *
1624
         * @name   Editor.setText
1625
         * @function
1626
         * @param {string} newText
1627
         */
1628
        me.setText = function(newText)
1629
        {
1630
            $message.val(newText);
1631
        }
1632
1633
        /**
1634
         * returns the current text
1635
         *
1636
         * @name   Editor.getText
1637
         * @function
1638
         * @return {string}
1639
         */
1640
        me.getText = function()
1641
        {
1642
            return $message.val()
1643
        }
1644
1645
        /**
1646
         * init status manager
1647
         *
1648
         * preloads jQuery elements
1649
         *
1650
         * @name   Editor.init
1651
         * @function
1652
         */
1653
        me.init = function()
1654
        {
1655
            $editorTabs = $('#editorTabs');
1656
            $message = $('#message');
1657
1658
            // bind events
1659
            $message.keydown(supportTabs);
1660
1661
            // bind click events to tab switchers (a), but save parent of them
1662
            // (li)
1663
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1664
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1665
        }
1666
1667
        return me;
1668
    })();
1669
1670
    /**
1671
     * (view) Parse and show paste.
1672
     *
1673
     * @name   PasteViewer
1674
     * @class
1675
     */
1676
    var PasteViewer = (function () {
1677
        var me = {};
1678
1679
        var $placeholder,
1680
            $prettyMessage,
1681
            $prettyPrint,
1682
            $plainText;
1683
1684
        var text,
1685
            format = 'plaintext',
1686
            isDisplayed = false,
1687
            isChanged = true; // by default true as nothing was parsed yet
1688
1689
        /**
1690
         * apply the set format on paste and displays it
1691
         *
1692
         * @name   PasteViewer.parsePaste
1693
         * @private
1694
         * @function
1695
         */
1696
        function parsePaste()
1697
        {
1698
            // skip parsing if no text is given
1699
            if (text === '') {
1700
                return;
1701
            }
1702
1703
            // set sanitized and linked text
1704
            var sanitizedLinkedText = DOMPurify.sanitize(Helper.urls2links(text));
1705
            $plainText.html(sanitizedLinkedText);
1706
            $prettyPrint.html(sanitizedLinkedText);
1707
1708
            switch (format) {
1709
                case 'markdown':
1710
                    var converter = new showdown.Converter({
1711
                        strikethrough: true,
1712
                        tables: true,
1713
                        tablesHeaderId: true
1714
                    });
1715
                    // let showdown convert the HTML and sanitize HTML *afterwards*!
1716
                    $plainText.html(
1717
                        DOMPurify.sanitize(converter.makeHtml(text))
1718
                    );
1719
                    // add table classes from bootstrap css
1720
                    $plainText.find('table').addClass('table-condensed table-bordered');
1721
                    break;
1722
                case 'syntaxhighlighting':
1723
                    // yes, this is really needed to initialize the environment
1724
                    if (typeof prettyPrint === 'function')
1725
                    {
1726
                        prettyPrint();
1727
                    }
1728
1729
                    $prettyPrint.html(
1730
                        DOMPurify.sanitize(
1731
                            prettyPrintOne(Helper.urls2links(text), null, true)
1732
                        )
1733
                    );
1734
                    // fall through, as the rest is the same
1735
                default: // = 'plaintext'
1736
                    $prettyPrint.css('white-space', 'pre-wrap');
1737
                    $prettyPrint.css('word-break', 'normal');
1738
                    $prettyPrint.removeClass('prettyprint');
1739
            }
1740
        }
1741
1742
        /**
1743
         * displays the paste
1744
         *
1745
         * @name   PasteViewer.showPaste
1746
         * @private
1747
         * @function
1748
         */
1749
        function showPaste()
1750
        {
1751
            // instead of "nothing" better display a placeholder
1752
            if (text === '') {
1753
                $placeholder.removeClass('hidden')
1754
                return;
1755
            }
1756
            // otherwise hide the placeholder
1757
            $placeholder.addClass('hidden')
1758
1759
            switch (format) {
1760
                case 'markdown':
1761
                    $plainText.removeClass('hidden');
1762
                    $prettyMessage.addClass('hidden');
1763
                    break;
1764
                default:
1765
                    $plainText.addClass('hidden');
1766
                    $prettyMessage.removeClass('hidden');
1767
                    break;
1768
            }
1769
        }
1770
1771
        /**
1772
         * sets the format in which the text is shown
1773
         *
1774
         * @name   PasteViewer.setFormat
1775
         * @function
1776
         * @param {string} newFormat the new format
1777
         */
1778
        me.setFormat = function(newFormat)
1779
        {
1780
            // skip if there is no update
1781
            if (format === newFormat) {
1782
                return;
1783
            }
1784
1785
            // needs to update display too, if we switch from or to Markdown
1786
            if (format === 'markdown' || newFormat === 'markdown') {
1787
                isDisplayed = false;
1788
            }
1789
1790
            format = newFormat;
1791
            isChanged = true;
1792
        }
1793
1794
        /**
1795
         * returns the current format
1796
         *
1797
         * @name   PasteViewer.getFormat
1798
         * @function
1799
         * @return {string}
1800
         */
1801
        me.getFormat = function()
1802
        {
1803
            return format;
1804
        }
1805
1806
        /**
1807
         * returns whether the current view is pretty printed
1808
         *
1809
         * @name   PasteViewer.isPrettyPrinted
1810
         * @function
1811
         * @return {bool}
1812
         */
1813
        me.isPrettyPrinted = function()
1814
        {
1815
            return $prettyPrint.hasClass('prettyprinted');
1816
        }
1817
1818
        /**
1819
         * sets the text to show
1820
         *
1821
         * @name   PasteViewer.setText
1822
         * @function
1823
         * @param {string} newText the text to show
1824
         */
1825
        me.setText = function(newText)
1826
        {
1827
            // escape HTML entities
1828
            newText = $('<div />').text(newText).html();
1829
            if (text !== newText) {
1830
                text = newText;
1831
                isChanged = true;
1832
            }
1833
        }
1834
1835
        /**
1836
         * gets the current cached text
1837
         *
1838
         * @name   PasteViewer.getText
1839
         * @function
1840
         * @return {string}
1841
         */
1842
        me.getText = function()
1843
        {
1844
            return text;
1845
        }
1846
1847
        /**
1848
         * show/update the parsed text (preview)
1849
         *
1850
         * @name   PasteViewer.run
1851
         * @function
1852
         */
1853
        me.run = function()
1854
        {
1855
            if (isChanged) {
1856
                parsePaste();
1857
                isChanged = false;
1858
            }
1859
1860
            if (!isDisplayed) {
1861
                showPaste();
1862
                isDisplayed = true;
1863
            }
1864
        }
1865
1866
        /**
1867
         * hide parsed text (preview)
1868
         *
1869
         * @name   PasteViewer.hide
1870
         * @function
1871
         */
1872
        me.hide = function()
1873
        {
1874
            if (!isDisplayed) {
1875
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1876
            }
1877
1878
            $plainText.addClass('hidden');
1879
            $prettyMessage.addClass('hidden');
1880
            $placeholder.addClass('hidden');
1881
1882
            isDisplayed = false;
1883
        }
1884
1885
        /**
1886
         * init status manager
1887
         *
1888
         * preloads jQuery elements
1889
         *
1890
         * @name   PasteViewer.init
1891
         * @function
1892
         */
1893
        me.init = function()
1894
        {
1895
            $placeholder = $('#placeholder');
1896
            $plainText = $('#plaintext');
1897
            $prettyMessage = $('#prettymessage');
1898
            $prettyPrint = $('#prettyprint');
1899
1900
            // check requirements
1901
            if (typeof prettyPrintOne !== 'function') {
1902
                Alert.showError([
1903
                    'The library %s is not available. This may cause display errors.',
1904
                    'pretty print'
1905
                ]);
1906
            }
1907
            if (typeof showdown !== 'object') {
1908
                Alert.showError([
1909
                    'The library %s is not available. This may cause display errors.',
1910
                    'showdown'
1911
                ]);
1912
            }
1913
1914
            // get default option from template/HTML or fall back to set value
1915
            format = Model.getFormatDefault() || format;
1916
            text = '';
1917
            isDisplayed = false;
1918
            isChanged = true;
1919
        }
1920
1921
        return me;
1922
    })();
1923
1924
    /**
1925
     * (view) Show attachment and preview if possible
1926
     *
1927
     * @name   AttachmentViewer
1928
     * @param  {object} window
1929
     * @param  {object} document
1930
     * @class
1931
     */
1932
    var AttachmentViewer = (function (window, document) {
1933
        var me = {};
1934
1935
        var $attachmentLink,
1936
            $attachmentPreview,
1937
            $attachment;
1938
1939
        var attachmentHasPreview = false;
1940
1941
        /**
1942
         * sets the attachment but does not yet show it
1943
         *
1944
         * @name   AttachmentViewer.setAttachment
1945
         * @function
1946
         * @param {string} attachmentData - base64-encoded data of file
1947
         * @param {string} fileName - optional, file name
1948
         */
1949
        me.setAttachment = function(attachmentData, fileName)
1950
        {
1951
            var imagePrefix = 'data:image/';
1952
1953
            $attachmentLink.attr('href', attachmentData);
1954
            if (typeof fileName !== 'undefined') {
1955
                $attachmentLink.attr('download', fileName);
1956
            }
1957
1958
            // if the attachment is an image, display it
1959
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
1960
                $attachmentPreview.html(
1961
                    $(document.createElement('img'))
1962
                        .attr('src', attachmentData)
1963
                        .attr('class', 'img-thumbnail')
1964
                );
1965
                attachmentHasPreview = true;
1966
            }
1967
        }
1968
1969
        /**
1970
         * displays the attachment
1971
         *
1972
         * @name AttachmentViewer.showAttachment
1973
         * @function
1974
         */
1975
        me.showAttachment = function()
1976
        {
1977
            $attachment.removeClass('hidden');
1978
1979
            if (attachmentHasPreview) {
1980
                $attachmentPreview.removeClass('hidden');
1981
            }
1982
        }
1983
1984
        /**
1985
         * removes the attachment
1986
         *
1987
         * This automatically hides the attachment containers to, to
1988
         * prevent an inconsistent display.
1989
         *
1990
         * @name AttachmentViewer.removeAttachment
1991
         * @function
1992
         */
1993
        me.removeAttachment = function()
1994
        {
1995
            me.hideAttachment();
1996
            me.hideAttachmentPreview();
1997
            $attachmentLink.prop('href', '');
1998
            $attachmentLink.prop('download', '');
1999
            $attachmentPreview.html('');
2000
        }
2001
2002
        /**
2003
         * hides the attachment
2004
         *
2005
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2006
         * for that) nor will it hide the attachment link if it was moved somewhere
2007
         * else (see AttachmentViewer.moveAttachmentTo).
2008
         *
2009
         * @name AttachmentViewer.hideAttachment
2010
         * @function
2011
         */
2012
        me.hideAttachment = function()
2013
        {
2014
            $attachment.addClass('hidden');
2015
        }
2016
2017
        /**
2018
         * hides the attachment preview
2019
         *
2020
         * @name AttachmentViewer.hideAttachmentPreview
2021
         * @function
2022
         */
2023
        me.hideAttachmentPreview = function()
2024
        {
2025
            $attachmentPreview.addClass('hidden');
2026
        }
2027
2028
        /**
2029
         * checks if there is an attachment
2030
         *
2031
         * @name   AttachmentViewer.hasAttachment
2032
         * @function
2033
         */
2034
        me.hasAttachment = function()
2035
        {
2036
            var link = $attachmentLink.prop('href');
2037
            return (typeof link !== 'undefined' && link !== '')
2038
        }
2039
2040
        /**
2041
         * return the attachment
2042
         *
2043
         * @name   AttachmentViewer.getAttachment
2044
         * @function
2045
         * @returns {array}
2046
         */
2047
        me.getAttachment = function()
2048
        {
2049
            return [
2050
                $attachmentLink.prop('href'),
2051
                $attachmentLink.prop('download')
2052
            ];
2053
        }
2054
2055
        /**
2056
         * moves the attachment link to another element
2057
         *
2058
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2059
         *
2060
         * @name   AttachmentViewer.moveAttachmentTo
2061
         * @function
2062
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2063
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2064
         */
2065
        me.moveAttachmentTo = function($element, label)
2066
        {
2067
            // move elemement to new place
2068
            $attachmentLink.appendTo($element);
2069
2070
            // update text
2071
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2072
        }
2073
2074
        /**
2075
         * initiate
2076
         *
2077
         * preloads jQuery elements
2078
         *
2079
         * @name   AttachmentViewer.init
2080
         * @function
2081
         */
2082
        me.init = function()
2083
        {
2084
            $attachment = $('#attachment');
2085
            $attachmentLink = $('#attachment a');
2086
            $attachmentPreview = $('#attachmentPreview');
2087
        }
2088
2089
        return me;
2090
    })(window, document);
2091
2092
    /**
2093
     * (view) Shows discussion thread and handles replies
2094
     *
2095
     * @name   DiscussionViewer
2096
     * @param  {object} window
2097
     * @param  {object} document
2098
     * @class
2099
     */
2100
    var DiscussionViewer = (function (window, document) {
2101
        var me = {};
2102
2103
        var $commentTail,
2104
            $discussion,
2105
            $reply,
2106
            $replyMessage,
2107
            $replyNickname,
2108
            $replyStatus,
2109
            $commentContainer;
2110
2111
        var replyCommentId;
2112
2113
        /**
2114
         * initializes the templates
2115
         *
2116
         * @name   DiscussionViewer.initTemplates
2117
         * @private
2118
         * @function
2119
         */
2120
        function initTemplates()
2121
        {
2122
            $reply = Model.getTemplate('reply');
2123
            $replyMessage = $reply.find('#replymessage');
2124
            $replyNickname = $reply.find('#nickname');
2125
            $replyStatus = $reply.find('#replystatus');
2126
2127
            // cache jQuery elements
2128
            $commentTail = Model.getTemplate('commenttail');
2129
        }
2130
2131
        /**
2132
         * open the comment entry when clicking the "Reply" button of a comment
2133
         *
2134
         * @name   DiscussionViewer.openReply
2135
         * @private
2136
         * @function
2137
         * @param  {Event} event
2138
         */
2139
        function openReply(event)
2140
        {
2141
            var $source = $(event.target);
2142
2143
            // clear input
2144
            $replyMessage.val('');
2145
            $replyNickname.val('');
2146
2147
            // get comment id from source element
2148
            replyCommentId = $source.parent().prop('id').split('_')[1];
2149
2150
            // move to correct position
2151
            $source.after($reply);
2152
2153
            // show
2154
            $reply.removeClass('hidden');
2155
            $replyMessage.focus();
2156
2157
            event.preventDefault();
2158
        }
2159
2160
        /**
2161
         * custom handler for displaying notifications in own status message area
2162
         *
2163
         * @name   DiscussionViewer.handleNotification
2164
         * @function
2165
         * @param  {string} alertType
2166
         * @param  {jQuery} $element
2167
         * @param  {string|array} args
2168
         * @param  {string|null} icon
2169
         * @return {bool|jQuery}
2170
         */
2171
        me.handleNotification = function(alertType, $element, args, icon)
2172
        {
2173
            // ignore loading messages
2174
            if (alertType === 'loading') {
2175
                return false;
2176
            }
2177
2178
            if (alertType === 'danger') {
2179
                $replyStatus.removeClass('alert-info');
2180
                $replyStatus.addClass('alert-danger');
2181
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2182
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2183
            } else {
2184
                $replyStatus.removeClass('alert-danger');
2185
                $replyStatus.addClass('alert-info');
2186
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2187
                $replyStatus.find(':first').addClass('glyphicon-alert');
2188
            }
2189
2190
            return $replyStatus;
2191
        }
2192
2193
        /**
2194
         * adds another comment
2195
         *
2196
         * @name   DiscussionViewer.addComment
2197
         * @function
2198
         * @param {object} comment
2199
         * @param {string} commentText
2200
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2201
         */
2202
        me.addComment = function(comment, commentText, nickname, $place)
2203
        {
2204
            if (typeof $place === 'undefined') {
2205
                // starting point (default value/fallback)
2206
                $place = $commentContainer;
2207
2208
                // if parent comment exists
2209
                var $parentComment = $('#comment_' + comment.parentid);
2210
                if ($parentComment.length) {
2211
                    // use parent as position for noew comment, so it shifted
2212
                    // to the right
2213
                    $place = $parentComment;
2214
                }
2215
            }
2216
            if (commentText === '') {
2217
                commentText = 'comment decryption failed';
2218
            }
2219
2220
            // create new comment based on template
2221
            var $commentEntry = Model.getTemplate('comment');
2222
            $commentEntry.prop('id', 'comment_' + comment.id);
2223
            var $commentEntryData = $commentEntry.find('div.commentdata');
2224
2225
            // set & parse text
2226
            $commentEntryData.html(
2227
                DOMPurify.sanitize(
2228
                    Helper.urls2links(commentText)
2229
                )
2230
            );
2231
2232
            // set nickname
2233
            if (nickname.length > 0) {
2234
                $commentEntry.find('span.nickname').text(nickname);
2235
            } else {
2236
                $commentEntry.find('span.nickname').html('<i></i>');
2237
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2238
            }
2239
2240
            // set date
2241
            $commentEntry.find('span.commentdate')
2242
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2243
                      .attr('title', 'CommentID: ' + comment.id);
2244
2245
            // if an avatar is available, display it
2246
            if (comment.meta.vizhash) {
2247
                $commentEntry.find('span.nickname')
2248
                             .before(
2249
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2250
                             );
2251
                $(document).on('languageLoaded', function () {
2252
                    $commentEntry.find('img.vizhash')
2253
                                 .prop('title', I18n._('Avatar generated from IP address'));
2254
                });
2255
            }
2256
2257
            // finally append comment
2258
            $place.append($commentEntry);
2259
        }
2260
2261
        /**
2262
         * finishes the discussion area after last comment
2263
         *
2264
         * @name   DiscussionViewer.finishDiscussion
2265
         * @function
2266
         */
2267
        me.finishDiscussion = function()
2268
        {
2269
            // add 'add new comment' area
2270
            $commentContainer.append($commentTail);
2271
2272
            // show discussions
2273
            $discussion.removeClass('hidden');
2274
        }
2275
2276
        /**
2277
         * shows the discussion area
2278
         *
2279
         * @name   DiscussionViewer.showDiscussion
2280
         * @function
2281
         */
2282
        me.showDiscussion = function()
2283
        {
2284
            $discussion.removeClass('hidden');
2285
        }
2286
2287
        /**
2288
         * removes the old discussion and prepares everything for creating a new
2289
         * one.
2290
         *
2291
         * @name   DiscussionViewer.prepareNewDisucssion
2292
         * @function
2293
         */
2294
        me.prepareNewDisucssion = function()
2295
        {
2296
            $commentContainer.html('');
2297
            $discussion.addClass('hidden');
2298
2299
            // (re-)init templates
2300
            initTemplates();
2301
        }
2302
2303
        /**
2304
         * returns the user put into the reply form
2305
         *
2306
         * @name   DiscussionViewer.getReplyData
2307
         * @function
2308
         * @return {array}
2309
         */
2310
        me.getReplyData = function()
2311
        {
2312
            return [
2313
                $replyMessage.val(),
2314
                $replyNickname.val()
2315
            ];
2316
        }
2317
2318
        /**
2319
         * highlights a specific comment and scrolls to it if necessary
2320
         *
2321
         * @name   DiscussionViewer.highlightComment
2322
         * @function
2323
         * @param {string} commentId
2324
         * @param {bool} fadeOut - whether to fade out the comment
2325
         */
2326
        me.highlightComment = function(commentId, fadeOut)
2327
        {
2328
            var $comment = $('#comment_' + commentId);
2329
            // in case comment does not exist, cancel
2330
            if ($comment.length === 0) {
2331
                return;
2332
            }
2333
2334
            var highlightComment = function () {
2335
                $comment.addClass('highlight');
2336
                if (fadeOut === true) {
2337
                    setTimeout(function () {
2338
                        $comment.removeClass('highlight');
2339
                    }, 300);
2340
                }
2341
            }
2342
2343
            if (UiHelper.isVisible($comment)) {
2344
                return highlightComment();
2345
            }
2346
2347
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2348
        }
2349
2350
        /**
2351
         * returns the id of the parent comment the user is replying to
2352
         *
2353
         * @name   DiscussionViewer.getReplyCommentId
2354
         * @function
2355
         * @return {int|undefined}
2356
         */
2357
        me.getReplyCommentId = function()
2358
        {
2359
            return replyCommentId;
2360
        }
2361
2362
        /**
2363
         * initiate
2364
         *
2365
         * preloads jQuery elements
2366
         *
2367
         * @name   DiscussionViewer.init
2368
         * @function
2369
         */
2370
        me.init = function()
2371
        {
2372
            // bind events to templates (so they are later cloned)
2373
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2374
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2375
2376
            $commentContainer = $('#commentcontainer');
2377
            $discussion = $('#discussion');
2378
        }
2379
2380
        return me;
2381
    })(window, document);
2382
2383
    /**
2384
     * Manage top (navigation) bar
2385
     *
2386
     * @name   TopNav
2387
     * @param  {object} window
2388
     * @param  {object} document
2389
     * @class
2390
     */
2391
    var TopNav = (function (window, document) {
2392
        var me = {};
2393
2394
        var createButtonsDisplayed = false;
2395
        var viewButtonsDisplayed = false;
2396
2397
        var $attach,
2398
            $burnAfterReading,
2399
            $burnAfterReadingOption,
2400
            $cloneButton,
2401
            $customAttachment,
2402
            $expiration,
2403
            $fileRemoveButton,
2404
            $fileWrap,
2405
            $formatter,
2406
            $newButton,
2407
            $openDiscussion,
2408
            $openDiscussionOption,
2409
            $password,
2410
            $passwordInput,
2411
            $rawTextButton,
2412
            $qrCodeLink,
2413
            $sendButton;
2414
2415
        var pasteExpiration = '1week';
2416
2417
        /**
2418
         * set the expiration on bootstrap templates in dropdown
2419
         *
2420
         * @name   TopNav.updateExpiration
2421
         * @private
2422
         * @function
2423
         * @param  {Event} event
2424
         */
2425
        function updateExpiration(event)
2426
        {
2427
            // get selected option
2428
            var target = $(event.target);
2429
2430
            // update dropdown display and save new expiration time
2431
            $('#pasteExpirationDisplay').text(target.text());
2432
            pasteExpiration = target.data('expiration');
2433
2434
            event.preventDefault();
2435
        }
2436
2437
        /**
2438
         * set the format on bootstrap templates in dropdown
2439
         *
2440
         * @name   TopNav.updateFormat
2441
         * @private
2442
         * @function
2443
         * @param  {Event} event
2444
         */
2445
        function updateFormat(event)
2446
        {
2447
            // get selected option
2448
            var $target = $(event.target);
2449
2450
            // update dropdown display and save new format
2451
            var newFormat = $target.data('format');
2452
            $('#pasteFormatterDisplay').text($target.text());
2453
            PasteViewer.setFormat(newFormat);
2454
2455
            // update preview
2456
            if (Editor.isPreview()) {
2457
                PasteViewer.run();
2458
            }
2459
2460
            event.preventDefault();
2461
        }
2462
2463
        /**
2464
         * when "burn after reading" is checked, disable discussion
2465
         *
2466
         * @name   TopNav.changeBurnAfterReading
2467
         * @private
2468
         * @function
2469
         */
2470
        function changeBurnAfterReading()
2471
        {
2472
            if ($burnAfterReading.is(':checked')) {
2473
                $openDiscussionOption.addClass('buttondisabled');
2474
                $openDiscussion.prop('checked', false);
2475
2476
                // if button is actually disabled, force-enable it and uncheck other button
2477
                $burnAfterReadingOption.removeClass('buttondisabled');
2478
            } else {
2479
                $openDiscussionOption.removeClass('buttondisabled');
2480
            }
2481
        }
2482
2483
        /**
2484
         * when discussion is checked, disable "burn after reading"
2485
         *
2486
         * @name   TopNav.changeOpenDiscussion
2487
         * @private
2488
         * @function
2489
         */
2490
        function changeOpenDiscussion()
2491
        {
2492
            if ($openDiscussion.is(':checked')) {
2493
                $burnAfterReadingOption.addClass('buttondisabled');
2494
                $burnAfterReading.prop('checked', false);
2495
2496
                // if button is actually disabled, force-enable it and uncheck other button
2497
                $openDiscussionOption.removeClass('buttondisabled');
2498
            } else {
2499
                $burnAfterReadingOption.removeClass('buttondisabled');
2500
            }
2501
        }
2502
2503
        /**
2504
         * return raw text
2505
         *
2506
         * @name   TopNav.rawText
2507
         * @private
2508
         * @function
2509
         * @param  {Event} event
2510
         */
2511
        function rawText(event)
2512
        {
2513
            TopNav.hideAllButtons();
2514
            Alert.showLoading('Showing raw text…', 0, 'time');
2515
            var paste = PasteViewer.getText();
2516
2517
            // push a new state to allow back navigation with browser back button
2518
            history.pushState(
2519
                {type: 'raw'},
2520
                document.title,
2521
                // recreate paste URL
2522
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2523
                Model.getPasteKey()
2524
            );
2525
2526
            // we use text/html instead of text/plain to avoid a bug when
2527
            // reloading the raw text view (it reverts to type text/html)
2528
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2529
            var newDoc = document.open('text/html', 'replace');
2530
            newDoc.write('<!DOCTYPE html><html><head>');
2531
            for (var i = 0; i < $head.length; i++) {
2532
                newDoc.write($head[i].outerHTML);
2533
            }
2534
            newDoc.write('</head><body><pre>' + DOMPurify.sanitize(paste) + '</pre></body></html>');
2535
            newDoc.close();
2536
        }
2537
2538
        /**
2539
         * saves the language in a cookie and reloads the page
2540
         *
2541
         * @name   TopNav.setLanguage
2542
         * @private
2543
         * @function
2544
         * @param  {Event} event
2545
         */
2546
        function setLanguage(event)
2547
        {
2548
            document.cookie = 'lang=' + $(event.target).data('lang');
2549
            UiHelper.reloadHome();
2550
        }
2551
2552
        /**
2553
         * hides all messages and creates a new paste
2554
         *
2555
         * @name   TopNav.clickNewPaste
2556
         * @private
2557
         * @function
2558
         * @param  {Event} event
2559
         */
2560
        function clickNewPaste(event)
2561
        {
2562
            Controller.hideStatusMessages();
2563
            Controller.newPaste();
2564
        }
2565
2566
        /**
2567
         * removes the existing attachment
2568
         *
2569
         * @name   TopNav.removeAttachment
2570
         * @private
2571
         * @function
2572
         * @param  {Event} event
2573
         */
2574
        function removeAttachment(event)
2575
        {
2576
            // if custom attachment is used, remove it first
2577
            if (!$customAttachment.hasClass('hidden')) {
2578
                AttachmentViewer.removeAttachment();
2579
                $customAttachment.addClass('hidden');
2580
                $fileWrap.removeClass('hidden');
2581
            }
2582
2583
            // our up-to-date jQuery can handle it :)
2584
            $fileWrap.find('input').val('');
2585
2586
            // pevent '#' from appearing in the URL
2587
            event.preventDefault();
2588
        }
2589
2590
        /**
2591
         * Shows the QR code of the current paste (URL).
2592
         *
2593
         * @name   TopNav.displayQrCode
2594
         * @function
2595
         * @param  {Event} event
2596
         */
2597
        function displayQrCode(event)
2598
        {
2599
            var qrCanvas = kjua({
2600
                render: 'canvas',
2601
                text: window.location.href
2602
            });
2603
            $('#qrcode-display').html(qrCanvas);
2604
        }
2605
2606
        /**
2607
         * Shows all elements belonging to viwing an existing pastes
2608
         *
2609
         * @name   TopNav.showViewButtons
2610
         * @function
2611
         */
2612
        me.showViewButtons = function()
2613
        {
2614
            if (viewButtonsDisplayed) {
2615
                console.warn('showViewButtons: view buttons are already displayed');
2616
                return;
2617
            }
2618
2619
            $newButton.removeClass('hidden');
2620
            $cloneButton.removeClass('hidden');
2621
            $rawTextButton.removeClass('hidden');
2622
            $qrCodeLink.removeClass('hidden');
2623
2624
            viewButtonsDisplayed = true;
2625
        }
2626
2627
        /**
2628
         * Hides all elements belonging to existing pastes
2629
         *
2630
         * @name   TopNav.hideViewButtons
2631
         * @function
2632
         */
2633
        me.hideViewButtons = function()
2634
        {
2635
            if (!viewButtonsDisplayed) {
2636
                console.warn('hideViewButtons: view buttons are already hidden');
2637
                return;
2638
            }
2639
2640
            $newButton.addClass('hidden');
2641
            $cloneButton.addClass('hidden');
2642
            $rawTextButton.addClass('hidden');
2643
            $qrCodeLink.addClass('hidden');
2644
2645
            viewButtonsDisplayed = false;
2646
        }
2647
2648
        /**
2649
         * Hides all elements belonging to existing pastes
2650
         *
2651
         * @name   TopNav.hideAllButtons
2652
         * @function
2653
         */
2654
        me.hideAllButtons = function()
2655
        {
2656
            me.hideViewButtons();
2657
            me.hideCreateButtons();
2658
        }
2659
2660
        /**
2661
         * shows all elements needed when creating a new paste
2662
         *
2663
         * @name   TopNav.showCreateButtons
2664
         * @function
2665
         */
2666
        me.showCreateButtons = function()
2667
        {
2668
            if (createButtonsDisplayed) {
2669
                console.warn('showCreateButtons: create buttons are already displayed');
2670
                return;
2671
            }
2672
2673
            $sendButton.removeClass('hidden');
2674
            $expiration.removeClass('hidden');
2675
            $formatter.removeClass('hidden');
2676
            $burnAfterReadingOption.removeClass('hidden');
2677
            $openDiscussionOption.removeClass('hidden');
2678
            $newButton.removeClass('hidden');
2679
            $password.removeClass('hidden');
2680
            $attach.removeClass('hidden');
2681
2682
            createButtonsDisplayed = true;
2683
        }
2684
2685
        /**
2686
         * shows all elements needed when creating a new paste
2687
         *
2688
         * @name   TopNav.hideCreateButtons
2689
         * @function
2690
         */
2691
        me.hideCreateButtons = function()
2692
        {
2693
            if (!createButtonsDisplayed) {
2694
                console.warn('hideCreateButtons: create buttons are already hidden');
2695
                return;
2696
            }
2697
2698
            $newButton.addClass('hidden');
2699
            $sendButton.addClass('hidden');
2700
            $expiration.addClass('hidden');
2701
            $formatter.addClass('hidden');
2702
            $burnAfterReadingOption.addClass('hidden');
2703
            $openDiscussionOption.addClass('hidden');
2704
            $password.addClass('hidden');
2705
            $attach.addClass('hidden');
2706
2707
            createButtonsDisplayed = false;
2708
        }
2709
2710
        /**
2711
         * only shows the "new paste" button
2712
         *
2713
         * @name   TopNav.showNewPasteButton
2714
         * @function
2715
         */
2716
        me.showNewPasteButton = function()
2717
        {
2718
            $newButton.removeClass('hidden');
2719
        }
2720
2721
        /**
2722
         * only hides the clone button
2723
         *
2724
         * @name   TopNav.hideCloneButton
2725
         * @function
2726
         */
2727
        me.hideCloneButton = function()
2728
        {
2729
            $cloneButton.addClass('hidden');
2730
        }
2731
2732
        /**
2733
         * only hides the raw text button
2734
         *
2735
         * @name   TopNav.hideRawButton
2736
         * @function
2737
         */
2738
        me.hideRawButton = function()
2739
        {
2740
            $rawTextButton.addClass('hidden');
2741
        }
2742
2743
        /**
2744
         * hides the file selector in attachment
2745
         *
2746
         * @name   TopNav.hideFileSelector
2747
         * @function
2748
         */
2749
        me.hideFileSelector = function()
2750
        {
2751
            $fileWrap.addClass('hidden');
2752
        }
2753
2754
2755
        /**
2756
         * shows the custom attachment
2757
         *
2758
         * @name   TopNav.showCustomAttachment
2759
         * @function
2760
         */
2761
        me.showCustomAttachment = function()
2762
        {
2763
            $customAttachment.removeClass('hidden');
2764
        }
2765
2766
        /**
2767
         * collapses the navigation bar if nedded
2768
         *
2769
         * @name   TopNav.collapseBar
2770
         * @function
2771
         */
2772
        me.collapseBar = function()
2773
        {
2774
            var $bar = $('.navbar-toggle');
2775
2776
            // check if bar is expanded
2777
            if ($bar.hasClass('collapse in')) {
2778
                // if so, toggle it
2779
                $bar.click();
2780
            }
2781
        }
2782
2783
        /**
2784
         * returns the currently set expiration time
2785
         *
2786
         * @name   TopNav.getExpiration
2787
         * @function
2788
         * @return {int}
2789
         */
2790
        me.getExpiration = function()
2791
        {
2792
            return pasteExpiration;
2793
        }
2794
2795
        /**
2796
         * returns the currently selected file(s)
2797
         *
2798
         * @name   TopNav.getFileList
2799
         * @function
2800
         * @return {FileList|null}
2801
         */
2802
        me.getFileList = function()
2803
        {
2804
            var $file = $('#file');
2805
2806
            // if no file given, return null
2807
            if (!$file.length || !$file[0].files.length) {
2808
                return null;
2809
            }
2810
            // @TODO is this really necessary
2811
            if (!($file[0].files && $file[0].files[0])) {
2812
                return null;
2813
            }
2814
2815
            return $file[0].files;
2816
        }
2817
2818
        /**
2819
         * returns the state of the burn after reading checkbox
2820
         *
2821
         * @name   TopNav.getExpiration
2822
         * @function
2823
         * @return {bool}
2824
         */
2825
        me.getBurnAfterReading = function()
2826
        {
2827
            return $burnAfterReading.is(':checked');
2828
        }
2829
2830
        /**
2831
         * returns the state of the discussion checkbox
2832
         *
2833
         * @name   TopNav.getOpenDiscussion
2834
         * @function
2835
         * @return {bool}
2836
         */
2837
        me.getOpenDiscussion = function()
2838
        {
2839
            return $openDiscussion.is(':checked');
2840
        }
2841
2842
        /**
2843
         * returns the entered password
2844
         *
2845
         * @name   TopNav.getPassword
2846
         * @function
2847
         * @return {string}
2848
         */
2849
        me.getPassword = function()
2850
        {
2851
            return $passwordInput.val();
2852
        }
2853
2854
        /**
2855
         * returns the element where custom attachments can be placed
2856
         *
2857
         * Used by AttachmentViewer when an attachment is cloned here.
2858
         *
2859
         * @name   TopNav.getCustomAttachment
2860
         * @function
2861
         * @return {jQuery}
2862
         */
2863
        me.getCustomAttachment = function()
2864
        {
2865
            return $customAttachment;
2866
        }
2867
2868
        /**
2869
         * init navigation manager
2870
         *
2871
         * preloads jQuery elements
2872
         *
2873
         * @name   TopNav.init
2874
         * @function
2875
         */
2876
        me.init = function()
2877
        {
2878
            $attach = $('#attach');
2879
            $burnAfterReading = $('#burnafterreading');
2880
            $burnAfterReadingOption = $('#burnafterreadingoption');
2881
            $cloneButton = $('#clonebutton');
2882
            $customAttachment = $('#customattachment');
2883
            $expiration = $('#expiration');
2884
            $fileRemoveButton = $('#fileremovebutton');
2885
            $fileWrap = $('#filewrap');
2886
            $formatter = $('#formatter');
2887
            $newButton = $('#newbutton');
2888
            $openDiscussion = $('#opendiscussion');
2889
            $openDiscussionOption = $('#opendiscussionoption');
2890
            $password = $('#password');
2891
            $passwordInput = $('#passwordinput');
2892
            $rawTextButton = $('#rawtextbutton');
2893
            $sendButton = $('#sendbutton');
2894
            $qrCodeLink = $('#qrcodelink');
2895
2896
            // bootstrap template drop down
2897
            $('#language ul.dropdown-menu li a').click(setLanguage);
2898
            // page template drop down
2899
            $('#language select option').click(setLanguage);
2900
2901
            // bind events
2902
            $burnAfterReading.change(changeBurnAfterReading);
2903
            $openDiscussionOption.change(changeOpenDiscussion);
2904
            $newButton.click(clickNewPaste);
2905
            $sendButton.click(PasteEncrypter.sendPaste);
2906
            $cloneButton.click(Controller.clonePaste);
2907
            $rawTextButton.click(rawText);
2908
            $fileRemoveButton.click(removeAttachment);
2909
            $qrCodeLink.click(displayQrCode);
2910
2911
            // bootstrap template drop downs
2912
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2913
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2914
2915
            // initiate default state of checkboxes
2916
            changeBurnAfterReading();
2917
            changeOpenDiscussion();
2918
2919
            // get default value from template or fall back to set value
2920
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2921
        }
2922
2923
        return me;
2924
    })(window, document);
2925
2926
    /**
2927
     * Responsible for AJAX requests, transparently handles encryption…
2928
     *
2929
     * @name   Uploader
2930
     * @class
2931
     */
2932
    var Uploader = (function () {
2933
        var me = {};
2934
2935
        var successFunc = null,
2936
            failureFunc = null,
2937
            url,
2938
            data,
2939
            symmetricKey,
2940
            password;
2941
2942
        /**
2943
         * public variable ('constant') for errors to prevent magic numbers
2944
         *
2945
         * @name   Uploader.error
2946
         * @readonly
2947
         * @enum   {Object}
2948
         */
2949
        me.error = {
2950
            okay: 0,
2951
            custom: 1,
2952
            unknown: 2,
2953
            serverError: 3
2954
        };
2955
2956
        /**
2957
         * ajaxHeaders to send in AJAX requests
2958
         *
2959
         * @name   Uploader.ajaxHeaders
2960
         * @private
2961
         * @readonly
2962
         * @enum   {Object}
2963
         */
2964
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2965
2966
        /**
2967
         * called after successful upload
2968
         *
2969
         * @name   Uploader.checkCryptParameters
2970
         * @private
2971
         * @function
2972
         * @throws {string}
2973
         */
2974
        function checkCryptParameters()
2975
        {
2976
            // workaround for this nasty 'bug' in ECMAScript
2977
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
2978
            var typeOfKey = typeof symmetricKey;
2979
            if (symmetricKey === null) {
2980
                typeOfKey = 'null';
2981
            }
2982
2983
            // in case of missing preparation, throw error
2984
            switch (typeOfKey) {
2985
                case 'string':
2986
                    // already set, all right
2987
                    return;
2988
                case 'null':
2989
                    // needs to be generated auto-generate
2990
                    symmetricKey = CryptTool.getSymmetricKey();
2991
                    break;
2992
                default:
2993
                    console.error('current invalid symmetricKey:', symmetricKey);
2994
                    throw 'symmetricKey is invalid, probably the module was not prepared';
2995
            }
2996
            // password is optional
2997
        }
2998
2999
        /**
3000
         * called after successful upload
3001
         *
3002
         * @name   Uploader.success
3003
         * @private
3004
         * @function
3005
         * @param {int} status
3006
         * @param {int} result - optional
3007
         */
3008
        function success(status, result)
3009
        {
3010
            // add useful data to result
3011
            result.encryptionKey = symmetricKey;
3012
            result.requestData = data;
3013
3014
            if (successFunc !== null) {
3015
                successFunc(status, result);
3016
            }
3017
        }
3018
3019
        /**
3020
         * called after a upload failure
3021
         *
3022
         * @name   Uploader.fail
3023
         * @private
3024
         * @function
3025
         * @param {int} status - internal code
3026
         * @param {int} result - original error code
3027
         */
3028
        function fail(status, result)
3029
        {
3030
            if (failureFunc !== null) {
3031
                failureFunc(status, result);
3032
            }
3033
        }
3034
3035
        /**
3036
         * actually uploads the data
3037
         *
3038
         * @name   Uploader.run
3039
         * @function
3040
         */
3041
        me.run = function()
3042
        {
3043
            $.ajax({
3044
                type: 'POST',
3045
                url: url,
3046
                data: data,
3047
                dataType: 'json',
3048
                headers: ajaxHeaders,
3049
                success: function(result) {
3050
                    if (result.status === 0) {
3051
                        success(0, result);
3052
                    } else if (result.status === 1) {
3053
                        fail(1, result);
3054
                    } else {
3055
                        fail(2, result);
3056
                    }
3057
                }
3058
            })
3059
            .fail(function(jqXHR, textStatus, errorThrown) {
3060
                console.error(textStatus, errorThrown);
3061
                fail(3, jqXHR);
3062
            });
3063
        }
3064
3065
        /**
3066
         * set success function
3067
         *
3068
         * @name   Uploader.setUrl
3069
         * @function
3070
         * @param {function} newUrl
3071
         */
3072
        me.setUrl = function(newUrl)
3073
        {
3074
            url = newUrl;
3075
        }
3076
3077
        /**
3078
         * sets the password to use (first value) and optionally also the
3079
         * encryption key (not recommend, it is automatically generated).
3080
         *
3081
         * Note: Call this after prepare() as prepare() resets these values.
3082
         *
3083
         * @name   Uploader.setCryptValues
3084
         * @function
3085
         * @param {string} newPassword
3086
         * @param {string} newKey       - optional
3087
         */
3088
        me.setCryptParameters = function(newPassword, newKey)
3089
        {
3090
            password = newPassword;
3091
3092
            if (typeof newKey !== 'undefined') {
3093
                symmetricKey = newKey;
3094
            }
3095
        }
3096
3097
        /**
3098
         * set success function
3099
         *
3100
         * @name   Uploader.setSuccess
3101
         * @function
3102
         * @param {function} func
3103
         */
3104
        me.setSuccess = function(func)
3105
        {
3106
            successFunc = func;
3107
        }
3108
3109
        /**
3110
         * set failure function
3111
         *
3112
         * @name   Uploader.setFailure
3113
         * @function
3114
         * @param {function} func
3115
         */
3116
        me.setFailure = function(func)
3117
        {
3118
            failureFunc = func;
3119
        }
3120
3121
        /**
3122
         * prepares a new upload
3123
         *
3124
         * Call this when doing a new upload to reset any data from potential
3125
         * previous uploads. Must be called before any other method of this
3126
         * module.
3127
         *
3128
         * @name   Uploader.prepare
3129
         * @function
3130
         * @return {object}
3131
         */
3132
        me.prepare = function()
3133
        {
3134
            // entropy should already be checked!
3135
3136
            // reset password
3137
            password = '';
3138
3139
            // reset key, so it a new one is generated when it is used
3140
            symmetricKey = null;
3141
3142
            // reset data
3143
            successFunc = null;
3144
            failureFunc = null;
3145
            url = Helper.baseUri();
3146
            data = {};
3147
        }
3148
3149
        /**
3150
         * encrypts and sets the data
3151
         *
3152
         * @name   Uploader.setData
3153
         * @function
3154
         * @param {string} index
3155
         * @param {mixed} element
3156
         */
3157
        me.setData = function(index, element)
3158
        {
3159
            checkCryptParameters();
3160
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3161
        }
3162
3163
        /**
3164
         * set the additional metadata to send unencrypted
3165
         *
3166
         * @name   Uploader.setUnencryptedData
3167
         * @function
3168
         * @param {string} index
3169
         * @param {mixed} element
3170
         */
3171
        me.setUnencryptedData = function(index, element)
3172
        {
3173
            data[index] = element;
3174
        }
3175
3176
        /**
3177
         * set the additional metadata to send unencrypted passed at once
3178
         *
3179
         * @name   Uploader.setUnencryptedData
3180
         * @function
3181
         * @param {object} newData
3182
         */
3183
        me.setUnencryptedBulkData = function(newData)
3184
        {
3185
            $.extend(data, newData);
3186
        }
3187
3188
        /**
3189
         * Helper, which parses shows a general error message based on the result of the Uploader
3190
         *
3191
         * @name    Uploader.parseUploadError
3192
         * @function
3193
         * @param {int} status
3194
         * @param {object} data
3195
         * @param {string} doThisThing - a human description of the action, which was tried
3196
         * @return {array}
3197
         */
3198
        me.parseUploadError = function(status, data, doThisThing) {
3199
            var errorArray;
3200
3201
            switch (status) {
3202
                case me.error['custom']:
0 ignored issues
show
The variable me seems to be never declared. If this is a global, consider adding a /** global: me */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
3203
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3204
                    break;
3205
                case me.error['unknown']:
3206
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3207
                    break;
3208
                case me.error['serverError']:
3209
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3210
                    break;
3211
                default:
3212
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3213
                    break;
3214
            }
3215
3216
            return errorArray;
3217
        }
3218
3219
        /**
3220
         * init Uploader
3221
         *
3222
         * @name   Uploader.init
3223
         * @function
3224
         */
3225
        me.init = function()
3226
        {
3227
            // nothing yet
3228
        }
3229
3230
        return me;
3231
    })();
3232
3233
    /**
3234
     * (controller) Responsible for encrypting paste and sending it to server.
3235
     *
3236
     * Does upload, encryption is done transparently by Uploader.
3237
     *
3238
     * @name PasteEncrypter
3239
     * @class
3240
     */
3241
    var PasteEncrypter = (function () {
3242
        var me = {};
3243
3244
        var requirementsChecked = false;
3245
3246
        /**
3247
         * checks whether there is a suitable amount of entrophy
3248
         *
3249
         * @name PasteEncrypter.checkRequirements
3250
         * @private
3251
         * @function
3252
         * @param {function} retryCallback - the callback to execute to retry the upload
3253
         * @return {bool}
3254
         */
3255
        function checkRequirements(retryCallback) {
3256
            // skip double requirement checks
3257
            if (requirementsChecked === true) {
3258
                return true;
3259
            }
3260
3261
            if (!CryptTool.isEntropyReady()) {
3262
                // display a message and wait
3263
                Alert.showStatus('Please move your mouse for more entropy…');
3264
3265
                CryptTool.addEntropySeedListener(retryCallback);
3266
                return false;
3267
            }
3268
3269
            requirementsChecked = true;
3270
3271
            return true;
3272
        }
3273
3274
        /**
3275
         * called after successful paste upload
3276
         *
3277
         * @name PasteEncrypter.showCreatedPaste
3278
         * @private
3279
         * @function
3280
         * @param {int} status
3281
         * @param {object} data
3282
         */
3283
        function showCreatedPaste(status, data) {
3284
            Alert.hideLoading();
3285
3286
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3287
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3288
3289
            Alert.hideMessages();
3290
3291
            // show notification
3292
            PasteStatus.createPasteNotification(url, deleteUrl)
3293
3294
            // show new URL in browser bar
3295
            history.pushState({type: 'newpaste'}, document.title, url);
3296
3297
            TopNav.showViewButtons();
3298
            TopNav.hideRawButton();
3299
            Editor.hide();
3300
3301
            // parse and show text
3302
            // (preparation already done in me.sendPaste())
3303
            PasteViewer.run();
3304
        }
3305
3306
        /**
3307
         * called after successful comment upload
3308
         *
3309
         * @name PasteEncrypter.showUploadedComment
3310
         * @private
3311
         * @function
3312
         * @param {int} status
3313
         * @param {object} data
3314
         */
3315
        function showUploadedComment(status, data) {
3316
            // show success message
3317
            // Alert.showStatus('Comment posted.');
3318
3319
            // reload paste
3320
            Controller.refreshPaste(function () {
3321
                // highlight sent comment
3322
                DiscussionViewer.highlightComment(data.id, true);
3323
                // reset error handler
3324
                Alert.setCustomHandler(null);
3325
            });
3326
        }
3327
3328
        /**
3329
         * adds attachments to the Uploader
3330
         *
3331
         * @name PasteEncrypter.encryptAttachments
3332
         * @private
3333
         * @function
3334
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3335
         * @param {function} callback - excuted when action is successful
3336
         */
3337
        function encryptAttachments(file, callback) {
3338
            if (typeof file !== 'undefined' && file !== null) {
3339
                // check file reader requirements for upload
3340
                if (typeof FileReader === 'undefined') {
3341
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3342
                    // cancels process as it does not execute callback
3343
                    return;
3344
                }
3345
3346
                var reader = new FileReader();
3347
3348
                // closure to capture the file information
3349
                reader.onload = function(event) {
3350
                    Uploader.setData('attachment', event.target.result);
3351
                    Uploader.setData('attachmentname', file.name);
3352
3353
                    // run callback
3354
                    return callback();
3355
                }
3356
3357
                // actually read first file
3358
                reader.readAsDataURL(file);
3359
            } else if (AttachmentViewer.hasAttachment()) {
3360
                // fall back to cloned part
3361
                var attachment = AttachmentViewer.getAttachment();
3362
3363
                Uploader.setData('attachment', attachment[0]);
3364
                Uploader.setData('attachmentname', attachment[1]);
3365
                return callback();
3366
            } else {
3367
                // if there are no attachments, this is of course still successful
3368
                return callback();
3369
            }
3370
        }
3371
3372
        /**
3373
         * send a reply in a discussion
3374
         *
3375
         * @name   PasteEncrypter.sendComment
3376
         * @function
3377
         */
3378
        me.sendComment = function()
3379
        {
3380
            Alert.hideMessages();
3381
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3382
3383
            // UI loading state
3384
            TopNav.hideAllButtons();
3385
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3386
3387
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3388
            var replyData = DiscussionViewer.getReplyData(),
3389
                plainText = replyData[0],
3390
                nickname = replyData[1],
3391
                parentid = DiscussionViewer.getReplyCommentId();
3392
3393
            // do not send if there is no data
3394
            if (plainText.length === 0) {
3395
                // revert loading status…
3396
                Alert.hideLoading();
3397
                Alert.setCustomHandler(null);
3398
                TopNav.showViewButtons();
3399
                return;
3400
            }
3401
3402
            // check entropy
3403
            if (!checkRequirements(function () {
3404
                me.sendComment();
3405
            })) {
3406
                return; // to prevent multiple executions
3407
            }
3408
            Alert.showLoading(null, 10);
3409
3410
            // prepare Uploader
3411
            Uploader.prepare();
3412
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3413
3414
            // set success/fail functions
3415
            Uploader.setSuccess(showUploadedComment);
3416
            Uploader.setFailure(function (status, data) {
3417
                // revert loading status…
3418
                Alert.hideLoading();
3419
                TopNav.showViewButtons();
3420
3421
                // show error message
3422
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3423
3424
                // reset error handler
3425
                Alert.setCustomHandler(null);
3426
            });
3427
3428
            // fill it with unencrypted params
3429
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3430
            if (typeof parentid === 'undefined') {
3431
                // if parent id is not set, this is the top-most comment, so use
3432
                // paste id as parent @TODO is this really good?
3433
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3434
            } else {
3435
                Uploader.setUnencryptedData('parentid', parentid);
3436
            }
3437
3438
            // encrypt data
3439
            Uploader.setData('data', plainText);
3440
3441
            if (nickname.length > 0) {
3442
                Uploader.setData('nickname', nickname);
3443
            }
3444
3445
            Uploader.run();
3446
        }
3447
3448
        /**
3449
         * sends a new paste to server
3450
         *
3451
         * @name   PasteEncrypter.sendPaste
3452
         * @function
3453
         */
3454
        me.sendPaste = function()
3455
        {
3456
            // hide previous (error) messages
3457
            Controller.hideStatusMessages();
3458
3459
            // UI loading state
3460
            TopNav.hideAllButtons();
3461
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3462
            TopNav.collapseBar();
3463
3464
            // get data
3465
            var plainText = Editor.getText(),
3466
                format = PasteViewer.getFormat(),
3467
                files = TopNav.getFileList();
3468
3469
            // do not send if there is no data
3470
            if (plainText.length === 0 && files === null) {
3471
                // revert loading status…
3472
                Alert.hideLoading();
3473
                TopNav.showCreateButtons();
3474
                return;
3475
            }
3476
3477
            Alert.showLoading(null, 10);
3478
3479
            // check entropy
3480
            if (!checkRequirements(function () {
3481
                me.sendPaste();
3482
            })) {
3483
                return; // to prevent multiple executions
3484
            }
3485
3486
            // prepare Uploader
3487
            Uploader.prepare();
3488
            Uploader.setCryptParameters(TopNav.getPassword());
3489
3490
            // set success/fail functions
3491
            Uploader.setSuccess(showCreatedPaste);
3492
            Uploader.setFailure(function (status, data) {
3493
                // revert loading status…
3494
                Alert.hideLoading();
3495
                TopNav.showCreateButtons();
3496
3497
                // show error message
3498
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3499
            });
3500
3501
            // fill it with unencrypted submitted options
3502
            Uploader.setUnencryptedBulkData({
3503
                expire:           TopNav.getExpiration(),
3504
                formatter:        format,
3505
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3506
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3507
            });
3508
3509
            // prepare PasteViewer for later preview
3510
            PasteViewer.setText(plainText);
3511
            PasteViewer.setFormat(format);
3512
3513
            // encrypt cipher data
3514
            Uploader.setData('data', plainText);
3515
3516
            // encrypt attachments
3517
            encryptAttachments(
3518
                files === null ? null : files[0],
3519
                function () {
3520
                    // send data
3521
                    Uploader.run();
3522
                }
3523
            );
3524
        }
3525
3526
        /**
3527
         * initialize
3528
         *
3529
         * @name   PasteEncrypter.init
3530
         * @function
3531
         */
3532
        me.init = function()
3533
        {
3534
            // nothing yet
3535
        }
3536
3537
        return me;
3538
    })();
3539
3540
    /**
3541
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3542
     *
3543
     * Only decryption, no download.
3544
     *
3545
     * @name PasteDecrypter
3546
     * @class
3547
     */
3548
    var PasteDecrypter = (function () {
3549
        var me = {};
3550
3551
        /**
3552
         * decrypt data or prompts for password in cvase of failure
3553
         *
3554
         * @name   PasteDecrypter.decryptOrPromptPassword
3555
         * @private
3556
         * @function
3557
         * @param  {string} key
3558
         * @param  {string} password - optional, may be an empty string
3559
         * @param  {string} cipherdata
3560
         * @throws {string}
3561
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3562
         */
3563
        function decryptOrPromptPassword(key, password, cipherdata)
3564
        {
3565
            // try decryption without password
3566
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3567
3568
            // if it fails, request password
3569
            if (plaindata.length === 0 && password.length === 0) {
3570
                // try to get cached password first
3571
                password = Prompt.getPassword();
3572
3573
                // if password is there, re-try
3574
                if (password.length === 0) {
3575
                    password = Prompt.requestPassword();
3576
                }
3577
                // recursive
3578
                // note: an infinite loop is prevented as the previous if
3579
                // clause checks whether a password is already set and ignores
3580
                // errors when a password has been passed
3581
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3582
            }
3583
3584
            // if all tries failed, we can only return an error
3585
            if (plaindata.length === 0) {
3586
                throw 'failed to decipher data';
3587
            }
3588
3589
            return plaindata;
3590
        }
3591
3592
        /**
3593
         * decrypt the actual paste text
3594
         *
3595
         * @name   PasteDecrypter.decryptOrPromptPassword
3596
         * @private
3597
         * @function
3598
         * @param  {object} paste - paste data in object form
3599
         * @param  {string} key
3600
         * @param  {string} password
3601
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3602
         * @return {bool} whether action was successful
3603
         * @throws {string}
3604
         */
3605
        function decryptPaste(paste, key, password, ignoreError)
3606
        {
3607
            var plaintext
3608
            if (ignoreError === true) {
3609
                plaintext = CryptTool.decipher(key, password, paste.data);
3610
            } else {
3611
                try {
3612
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3613
                } catch (err) {
3614
                    throw 'failed to decipher paste text: ' + err
3615
                }
3616
                if (plaintext === false) {
3617
                    return false;
3618
                }
3619
            }
3620
3621
            // on success show paste
3622
            PasteViewer.setFormat(paste.meta.formatter);
3623
            PasteViewer.setText(plaintext);
3624
            // trigger to show the text (attachment loaded afterwards)
3625
            PasteViewer.run();
3626
3627
            return true;
3628
        }
3629
3630
        /**
3631
         * decrypts any attachment
3632
         *
3633
         * @name   PasteDecrypter.decryptAttachment
3634
         * @private
3635
         * @function
3636
         * @param  {object} paste - paste data in object form
3637
         * @param  {string} key
3638
         * @param  {string} password
3639
         * @return {bool} whether action was successful
3640
         * @throws {string}
3641
         */
3642
        function decryptAttachment(paste, key, password)
3643
        {
3644
            // decrypt attachment
3645
            try {
3646
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3647
            } catch (err) {
3648
                throw 'failed to decipher attachment: ' + err
3649
            }
3650
            if (attachment === false) {
3651
                return false;
3652
            }
3653
3654
            // decrypt attachment name
3655
            var attachmentName;
3656
            if (paste.attachmentname) {
3657
                try {
3658
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3659
                } catch (err) {
3660
                    throw 'failed to decipher attachment name: ' + err
3661
                }
3662
                if (attachmentName === false) {
3663
                    return false;
3664
                }
3665
            }
3666
3667
            AttachmentViewer.setAttachment(attachment, attachmentName);
3668
            AttachmentViewer.showAttachment();
3669
3670
            return true;
3671
        }
3672
3673
        /**
3674
         * decrypts all comments and shows them
3675
         *
3676
         * @name   PasteDecrypter.decryptComments
3677
         * @private
3678
         * @function
3679
         * @param  {object} paste - paste data in object form
3680
         * @param  {string} key
3681
         * @param  {string} password
3682
         * @return {bool} whether action was successful
3683
         */
3684
        function decryptComments(paste, key, password)
3685
        {
3686
            // remove potentially previous discussion
3687
            DiscussionViewer.prepareNewDisucssion();
3688
3689
            // iterate over comments
3690
            for (var i = 0; i < paste.comments.length; ++i) {
3691
                var comment = paste.comments[i];
3692
3693
                DiscussionViewer.addComment(
3694
                    comment,
3695
                    CryptTool.decipher(key, password, comment.data),
3696
                    CryptTool.decipher(key, password, comment.meta.nickname)
3697
                );
3698
            }
3699
3700
            DiscussionViewer.finishDiscussion();
3701
            DiscussionViewer.showDiscussion();
3702
            return true;
3703
        }
3704
3705
        /**
3706
         * show decrypted text in the display area, including discussion (if open)
3707
         *
3708
         * @name   PasteDecrypter.run
3709
         * @function
3710
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3711
         */
3712
        me.run = function(paste)
3713
        {
3714
            Alert.hideMessages();
3715
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3716
3717
            if (typeof paste === 'undefined') {
3718
                paste = $.parseJSON(Model.getCipherData());
3719
            }
3720
3721
            var key = Model.getPasteKey(),
3722
                password = Prompt.getPassword();
3723
3724
            if (PasteViewer.isPrettyPrinted()) {
3725
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3726
                return;
3727
            }
3728
3729
            // try to decrypt the paste
3730
            try {
3731
                // decrypt attachments
3732
                if (paste.attachment) {
3733
                    // try to decrypt paste and if it fails (because the password is
3734
                    // missing) return to let JS continue and wait for user
3735
                    if (!decryptAttachment(paste, key, password)) {
3736
                        return;
3737
                    }
3738
                    // ignore empty paste, as this is allowed when pasting attachments
3739
                    decryptPaste(paste, key, password, true);
3740
                } else {
3741
                    decryptPaste(paste, key, password);
3742
                }
3743
3744
3745
                // shows the remaining time (until) deletion
3746
                PasteStatus.showRemainingTime(paste.meta);
3747
3748
                // if the discussion is opened on this paste, display it
3749
                if (paste.meta.opendiscussion) {
3750
                    decryptComments(paste, key, password);
3751
                }
3752
3753
                Alert.hideLoading();
3754
                TopNav.showViewButtons();
3755
            } catch(err) {
3756
                Alert.hideLoading();
3757
3758
                // log and show error
3759
                console.error(err);
3760
                Alert.showError('Could not decrypt data (Wrong key?)');
3761
            }
3762
        }
3763
3764
        /**
3765
         * initialize
3766
         *
3767
         * @name   PasteDecrypter.init
3768
         * @function
3769
         */
3770
        me.init = function()
3771
        {
3772
            // nothing yet
3773
        }
3774
3775
        return me;
3776
    })();
3777
3778
    /**
3779
     * (controller) main PrivateBin logic
3780
     *
3781
     * @name   Controller
3782
     * @param  {object} window
3783
     * @param  {object} document
3784
     * @class
3785
     */
3786
    var Controller = (function (window, document) {
3787
        var me = {};
3788
3789
        /**
3790
         * hides all status messages no matter which module showed them
3791
         *
3792
         * @name   Controller.hideStatusMessages
3793
         * @function
3794
         */
3795
        me.hideStatusMessages = function()
3796
        {
3797
            PasteStatus.hideMessages();
3798
            Alert.hideMessages();
3799
        }
3800
3801
        /**
3802
         * creates a new paste
3803
         *
3804
         * @name   Controller.newPaste
3805
         * @function
3806
         */
3807
        me.newPaste = function()
3808
        {
3809
            // Important: This *must not* run Alert.hideMessages() as previous
3810
            // errors from viewing a paste should be shown.
3811
            TopNav.hideAllButtons();
3812
            Alert.showLoading('Preparing new paste…', 0, 'time');
3813
3814
            PasteStatus.hideMessages();
3815
            PasteViewer.hide();
3816
            Editor.resetInput();
3817
            Editor.show();
3818
            Editor.focusInput();
3819
3820
            TopNav.showCreateButtons();
3821
            Alert.hideLoading();
3822
        }
3823
3824
        /**
3825
         * shows the loaded paste
3826
         *
3827
         * @name   Controller.showPaste
3828
         * @function
3829
         */
3830
        me.showPaste = function()
3831
        {
3832
            try {
3833
                Model.getPasteId();
3834
                Model.getPasteKey();
3835
            } catch (err) {
3836
                console.error(err);
3837
3838
                // missing decryption key (or paste ID) in URL?
3839
                if (window.location.hash.length === 0) {
3840
                    Alert.showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)');
3841
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3842
                    return;
3843
                }
3844
            }
3845
3846
            // show proper elements on screen
3847
            PasteDecrypter.run();
3848
        }
3849
3850
        /**
3851
         * refreshes the loaded paste to show potential new data
3852
         *
3853
         * @name   Controller.refreshPaste
3854
         * @function
3855
         * @param  {function} callback
3856
         */
3857
        me.refreshPaste = function(callback)
3858
        {
3859
            // save window position to restore it later
3860
            var orgPosition = $(window).scrollTop();
3861
3862
            Uploader.prepare();
3863
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3864
3865
            Uploader.setFailure(function (status, data) {
3866
                // revert loading status…
3867
                Alert.hideLoading();
3868
                TopNav.showViewButtons();
3869
3870
                // show error message
3871
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3872
            })
3873
            Uploader.setSuccess(function (status, data) {
3874
                PasteDecrypter.run(data);
3875
3876
                // restore position
3877
                window.scrollTo(0, orgPosition);
3878
3879
                callback();
3880
            })
3881
            Uploader.run();
3882
        }
3883
3884
        /**
3885
         * clone the current paste
3886
         *
3887
         * @name   Controller.clonePaste
3888
         * @function
3889
         * @param  {Event} event
3890
         */
3891
        me.clonePaste = function(event)
3892
        {
3893
            TopNav.collapseBar();
3894
            TopNav.hideAllButtons();
3895
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3896
3897
            // hide messages from previous paste
3898
            me.hideStatusMessages();
3899
3900
            // erase the id and the key in url
3901
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3902
3903
            if (AttachmentViewer.hasAttachment()) {
3904
                AttachmentViewer.moveAttachmentTo(
3905
                    TopNav.getCustomAttachment(),
3906
                    'Cloned: \'%s\''
3907
                );
3908
                TopNav.hideFileSelector();
3909
                AttachmentViewer.hideAttachment();
3910
                // NOTE: it also looks nice without removing the attachment
3911
                // but for a consistent display we remove it…
3912
                AttachmentViewer.hideAttachmentPreview();
3913
                TopNav.showCustomAttachment();
3914
3915
                // show another status message to make the user aware that the
3916
                // file was cloned too!
3917
                Alert.showStatus(
3918
                    [
3919
                        'The cloned file \'%s\' was attached to this paste.',
3920
                        AttachmentViewer.getAttachment()[1]
3921
                    ], 'copy', true, true);
3922
            }
3923
3924
            Editor.setText(PasteViewer.getText())
3925
            PasteViewer.hide();
3926
            Editor.show();
3927
3928
            Alert.hideLoading();
3929
            TopNav.showCreateButtons();
3930
        }
3931
3932
        /**
3933
         * removes a saved paste
3934
         *
3935
         * @name   Controller.removePaste
3936
         * @function
3937
         * @param  {string} pasteId
3938
         * @param  {string} deleteToken
3939
         */
3940
        me.removePaste = function(pasteId, deleteToken) {
3941
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3942
            // so we use a POST request
3943
            Uploader.prepare();
3944
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3945
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3946
3947
            Uploader.setFailure(function () {
3948
                Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3949
            })
3950
            Uploader.run();
3951
        }
3952
3953
        /**
3954
         * application start
3955
         *
3956
         * @name   Controller.init
3957
         * @function
3958
         */
3959
        me.init = function()
3960
        {
3961
            // first load translations
3962
            I18n.loadTranslations();
3963
3964
            DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
3965
3966
            // initialize other modules/"classes"
3967
            Alert.init();
3968
            Model.init();
3969
            AttachmentViewer.init();
3970
            DiscussionViewer.init();
3971
            Editor.init();
3972
            PasteDecrypter.init();
3973
            PasteEncrypter.init();
3974
            PasteStatus.init();
3975
            PasteViewer.init();
3976
            Prompt.init();
3977
            TopNav.init();
3978
            UiHelper.init();
3979
            Uploader.init();
3980
3981
            // display an existing paste
3982
            if (Model.hasCipherData()) {
3983
                return me.showPaste();
3984
            }
3985
3986
            // otherwise create a new paste
3987
            me.newPaste();
3988
        }
3989
3990
        return me;
3991
    })(window, document);
3992
3993
    return {
3994
        Helper: Helper,
3995
        I18n: I18n,
3996
        CryptTool: CryptTool,
3997
        Model: Model,
3998
        UiHelper: UiHelper,
3999
        Alert: Alert,
4000
        PasteStatus: PasteStatus,
4001
        Prompt: Prompt,
4002
        Editor: Editor,
4003
        PasteViewer: PasteViewer,
4004
        AttachmentViewer: AttachmentViewer,
4005
        DiscussionViewer: DiscussionViewer,
4006
        TopNav: TopNav,
4007
        Uploader: Uploader,
4008
        PasteEncrypter: PasteEncrypter,
4009
        PasteDecrypter: PasteDecrypter,
4010
        Controller: Controller
4011
    };
4012
}(jQuery, sjcl, Base64, RawDeflate);
4013