Passed
Push — master ( 81ac23...a5d5f6 )
by El
03:02
created

js/privatebin.js (36 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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: FileReader */
16
/** global: RawDeflate */
17
/** global: history */
18
/** global: navigator */
19
/** global: prettyPrint */
20
/** global: prettyPrintOne */
21
/** global: showdown */
22
/** global: sjcl */
23
24
// Immediately start random number generator collector.
25
sjcl.random.startCollectors();
26
27
// main application start, called when DOM is fully loaded
28
jQuery(document).ready(function() {
29
    // run main controller
30
    $.PrivateBin.Controller.init();
31
});
32
33
jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
34
    'use strict';
35
36
    /**
37
     * static Helper methods
38
     *
39
     * @name Helper
40
     * @class
41
     */
42
    var Helper = (function () {
43
        var me = {};
44
45
        /**
46
         * character to HTML entity lookup table
47
         *
48
         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
49
         * @name Helper.entityMap
50
         * @private
51
         * @enum   {Object}
52
         * @readonly
53
         */
54
        var entityMap = {
55
            '&': '&',
56
            '<': '&lt;',
57
            '>': '&gt;',
58
            '"': '&quot;',
59
            "'": '&#39;',
60
            '/': '&#x2F;',
61
            '`': '&#x60;',
62
            '=': '&#x3D;'
63
        };
64
65
        /**
66
         * cache for script location
67
         *
68
         * @name Helper.baseUri
69
         * @private
70
         * @enum   {string|null}
71
         */
72
        var baseUri = null;
73
74
        /**
75
         * converts a duration (in seconds) into human friendly approximation
76
         *
77
         * @name Helper.secondsToHuman
78
         * @function
79
         * @param  {number} seconds
80
         * @return {Array}
81
         */
82
        me.secondsToHuman = function(seconds)
83
        {
84
            var v;
85
            if (seconds < 60)
86
            {
87
                v = Math.floor(seconds);
88
                return [v, 'second'];
89
            }
90
            if (seconds < 60 * 60)
91
            {
92
                v = Math.floor(seconds / 60);
93
                return [v, 'minute'];
94
            }
95
            if (seconds < 60 * 60 * 24)
96
            {
97
                v = Math.floor(seconds / (60 * 60));
98
                return [v, 'hour'];
99
            }
100
            // If less than 2 months, display in days:
101
            if (seconds < 60 * 60 * 24 * 60)
102
            {
103
                v = Math.floor(seconds / (60 * 60 * 24));
104
                return [v, 'day'];
105
            }
106
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
107
            return [v, 'month'];
108
        }
109
110
        /**
111
         * text range selection
112
         *
113
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
114
         * @name   Helper.selectText
115
         * @function
116
         * @param  {HTMLElement} element
117
         */
118
        me.selectText = function(element)
119
        {
120
            var range, selection;
121
122
            // MS
123
            if (document.body.createTextRange) {
124
                range = document.body.createTextRange();
125
                range.moveToElementText(element);
126
                range.select();
127
            } else if (window.getSelection){
128
                selection = window.getSelection();
129
                range = document.createRange();
130
                range.selectNodeContents(element);
131
                selection.removeAllRanges();
132
                selection.addRange(range);
133
            }
134
        }
135
136
        /**
137
         * set text of a jQuery element (required for IE),
138
         *
139
         * @name   Helper.setElementText
140
         * @function
141
         * @param  {jQuery} $element - a jQuery element
142
         * @param  {string} text - the text to enter
143
         */
144
        me.setElementText = function($element, text)
145
        {
146
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
147
            if ($('#oldienotice').is(':visible')) {
148
                var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
149
                $element.html('<pre>' + html + '</pre>');
150
            }
151
            // for other (sane) browsers:
152
            else
153
            {
154
                $element.text(text);
155
            }
156
        }
157
158
        /**
159
         * convert URLs to clickable links.
160
         * URLs to handle:
161
         * <pre>
162
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
163
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
164
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
165
         * </pre>
166
         *
167
         * @name   Helper.urls2links
168
         * @function
169
         * @param  {Object} $element - a jQuery DOM element
170
         */
171
        me.urls2links = function($element)
172
        {
173
            var markup = '<a href="$1" rel="nofollow">$1</a>';
174
            $element.html(
175
                $element.html().replace(
176
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
177
                    markup
178
                )
179
            );
180
            $element.html(
181
                $element.html().replace(
182
                    /((magnet):[\w?=&.\/-;#@~%+*-]+)/ig,
183
                    markup
184
                )
185
            );
186
        }
187
188
        /**
189
         * minimal sprintf emulation for %s and %d formats
190
         *
191
         * Note that this function needs the parameters in the same order as the
192
         * format strings appear in the string, contrary to the original.
193
         *
194
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
195
         * @name   Helper.sprintf
196
         * @function
197
         * @param  {string} format
0 ignored issues
show
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
198
         * @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...
199
         * @return {string}
200
         */
201
        me.sprintf = function()
202
        {
203
            var args = Array.prototype.slice.call(arguments);
204
            var format = args[0],
205
                i = 1;
206
            return format.replace(/%(s|d)/g, function (m) {
207
                // m is the matched format, e.g. %s, %d
208
                var val = args[i];
209
                // A switch statement so that the formatter can be extended.
210
                switch (m)
211
                {
212
                    case '%d':
213
                        val = parseFloat(val);
214
                        if (isNaN(val)) {
215
                            val = 0;
216
                        }
217
                        break;
218
                    default:
219
                        // Default is %s
220
                }
221
                ++i;
222
                return val;
223
            });
224
        }
225
226
        /**
227
         * get value of cookie, if it was set, empty string otherwise
228
         *
229
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
230
         * @name   Helper.getCookie
231
         * @function
232
         * @param  {string} cname - may not be empty
233
         * @return {string}
234
         */
235
        me.getCookie = function(cname) {
236
            var name = cname + '=',
237
                ca = document.cookie.split(';');
238
            for (var i = 0; i < ca.length; ++i) {
239
                var c = ca[i];
240
                while (c.charAt(0) === ' ')
241
                {
242
                    c = c.substring(1);
243
                }
244
                if (c.indexOf(name) === 0)
245
                {
246
                    return c.substring(name.length, c.length);
247
                }
248
            }
249
            return '';
250
        }
251
252
        /**
253
         * get the current location (without search or hash part of the URL),
254
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
255
         *
256
         * @name   Helper.baseUri
257
         * @function
258
         * @return {string}
259
         */
260
        me.baseUri = function()
261
        {
262
            // check for cached version
263
            if (baseUri !== null) {
264
                return baseUri;
265
            }
266
267
            baseUri = window.location.origin + window.location.pathname;
268
            return baseUri;
269
        }
270
271
        /**
272
         * convert all applicable characters to HTML entities
273
         *
274
         * @see    {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
275
         * @name   Helper.htmlEntities
276
         * @function
277
         * @param  {string} str
278
         * @return {string} escaped HTML
279
         */
280
        me.htmlEntities = function(str) {
281
            return String(str).replace(
282
                /[&<>"'`=\/]/g, function(s) {
283
                    return entityMap[s];
284
                });
285
        }
286
287
        /**
288
         * resets state, used for unit testing
289
         *
290
         * @name   Helper.reset
291
         * @function
292
         */
293
        me.reset = function()
294
        {
295
            baseUri = null;
296
        }
297
298
        return me;
299
    })();
300
301
    /**
302
     * internationalization module
303
     *
304
     * @name I18n
305
     * @param  {object} window
0 ignored issues
show
The parameter window does not exist. Did you maybe forget to remove this comment?
Loading history...
306
     * @param  {object} document
0 ignored issues
show
The parameter document does not exist. Did you maybe forget to remove this comment?
Loading history...
307
     * @class
308
     */
309
    var I18n = (function () {
310
        var me = {};
311
312
        /**
313
         * const for string of loaded language
314
         *
315
         * @name I18n.languageLoadedEvent
316
         * @private
317
         * @prop   {string}
318
         * @readonly
319
         */
320
        var languageLoadedEvent = 'languageLoaded';
321
322
        /**
323
         * supported languages, minus the built in 'en'
324
         *
325
         * @name I18n.supportedLanguages
326
         * @private
327
         * @prop   {string[]}
328
         * @readonly
329
         */
330
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
331
332
        /**
333
         * built in language
334
         *
335
         * @name I18n.language
336
         * @private
337
         * @prop   {string|null}
338
         */
339
        var language = null;
340
341
        /**
342
         * translation cache
343
         *
344
         * @name I18n.translations
345
         * @private
346
         * @enum   {Object}
347
         */
348
        var translations = {};
349
350
        /**
351
         * translate a string, alias for I18n.translate
352
         *
353
         * @name   I18n._
354
         * @function
355
         * @param  {jQuery} $element - optional
0 ignored issues
show
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
356
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
357
         * @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...
358
         * @return {string}
359
         */
360
        me._ = function()
361
        {
362
            return me.translate.apply(this, arguments);
363
        }
364
365
        /**
366
         * translate a string
367
         *
368
         * Optionally pass a jQuery element as the first parameter, to automatically
369
         * let the text of this element be replaced. In case the (asynchronously
370
         * loaded) language is not downloadet yet, this will make sure the string
371
         * is replaced when it is actually loaded.
372
         * So for easy translations passing the jQuery object to apply it to is
373
         * more save, especially when they are loaded in the beginning.
374
         *
375
         * @name   I18n.translate
376
         * @function
377
         * @param  {jQuery} $element - optional
0 ignored issues
show
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
378
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
379
         * @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...
380
         * @return {string}
381
         */
382
        me.translate = function()
383
        {
384
            // convert parameters to array
385
            var args = Array.prototype.slice.call(arguments),
386
                messageId,
387
                $element = null;
388
389
            // parse arguments
390
            if (args[0] instanceof jQuery) {
391
                // optional jQuery element as first parameter
392
                $element = args[0];
393
                args.shift();
394
            }
395
396
            // extract messageId from arguments
397
            var usesPlurals = $.isArray(args[0]);
398
            if (usesPlurals) {
399
                // use the first plural form as messageId, otherwise the singular
400
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
401
            } else {
402
                messageId = args[0];
403
            }
404
405
            if (messageId.length === 0) {
406
                return messageId;
407
            }
408
409
            // if no translation string cannot be found (in translations object)
410
            if (!translations.hasOwnProperty(messageId) || language === null) {
411
                // if language is still loading and we have an elemt assigned
412
                if (language === null && $element !== null) {
413
                    // handle the error by attaching the language loaded event
414
                    var orgArguments = arguments;
415
                    $(document).on(languageLoadedEvent, function () {
416
                        // log to show that the previous error could be mitigated
417
                        console.log('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language);
0 ignored issues
show
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
418
                        // re-execute this function
419
                        me.translate.apply(this, orgArguments);
420
                    });
421
422
                    // and fall back to English for now until the real language
423
                    // file is loaded
424
                }
425
426
                // for all other langauges than English for which this behaviour
427
                // is expected as it is built-in, log error
428
                if (language !== null && language !== 'en') {
429
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
430
                    // fallback to English
431
                }
432
433
                // save English translation (should be the same on both sides)
434
                translations[messageId] = args[0];
435
            }
436
437
            // lookup plural translation
438
            if (usesPlurals && $.isArray(translations[messageId])) {
439
                var n = parseInt(args[1] || 1, 10),
440
                    key = me.getPluralForm(n),
441
                    maxKey = translations[messageId].length - 1;
442
                if (key > maxKey) {
443
                    key = maxKey;
444
                }
445
                args[0] = translations[messageId][key];
446
                args[1] = n;
447
            } else {
448
                // lookup singular translation
449
                args[0] = translations[messageId];
450
            }
451
452
            // format string
453
            var output = Helper.sprintf.apply(this, args);
454
455
            // if $element is given, apply text to element
456
            if ($element !== null) {
457
                // get last text node of element
458
                var content = $element.contents();
459
                if (content.length > 1) {
460
                    content[content.length - 1].nodeValue = ' ' + output;
461
                } else {
462
                    $element.text(output);
463
                }
464
            }
465
466
            return output;
467
        }
468
469
        /**
470
         * per language functions to use to determine the plural form
471
         *
472
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
473
         * @name   I18n.getPluralForm
474
         * @function
475
         * @param  {int} n
476
         * @return {int} array key
477
         */
478
        me.getPluralForm = function(n) {
479
            switch (language)
480
            {
481
                case 'fr':
482
                case 'oc':
483
                case 'zh':
484
                    return (n > 1 ? 1 : 0);
485
                case 'pl':
486
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
487
                case 'ru':
488
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
489
                case 'sl':
490
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
491
                // de, en, es, it, no, pt
492
                default:
493
                    return (n !== 1 ? 1 : 0);
494
            }
495
        }
496
497
        /**
498
         * load translations into cache
499
         *
500
         * @name   I18n.loadTranslations
501
         * @function
502
         */
503
        me.loadTranslations = function()
504
        {
505
            var newLanguage = Helper.getCookie('lang');
506
507
            // auto-select language based on browser settings
508
            if (newLanguage.length === 0) {
509
                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
510
            }
511
512
            // if language is already used skip update
513
            if (newLanguage === language) {
514
                return;
515
            }
516
517
            // if language is built-in (English) skip update
518
            if (newLanguage === 'en') {
519
                language = 'en';
520
                return;
521
            }
522
523
            // if language is not supported, show error
524
            if (supportedLanguages.indexOf(newLanguage) === -1) {
525
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
526
                language = 'en';
527
                return;
528
            }
529
530
            // load strings from JSON
531
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
532
                language = newLanguage;
533
                translations = data;
534
                $(document).triggerHandler(languageLoadedEvent);
535
            }).fail(function (data, textStatus, errorMsg) {
536
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
537
                language = 'en';
538
            });
539
        }
540
541
        /**
542
         * resets state, used for unit testing
543
         *
544
         * @name   I18n.reset
545
         * @function
546
         */
547
        me.reset = function(mockLanguage, mockTranslations)
548
        {
549
            language = mockLanguage || null;
550
            translations = mockTranslations || {};
551
        }
552
553
        return me;
554
    })();
555
556
    /**
557
     * handles everything related to en/decryption
558
     *
559
     * @name CryptTool
560
     * @class
561
     */
562
    var CryptTool = (function () {
563
        var me = {};
564
565
        /**
566
         * compress a message (deflate compression), returns base64 encoded data
567
         *
568
         * @name   CryptTool.compress
569
         * @function
570
         * @private
571
         * @param  {string} message
572
         * @return {string} base64 data
573
         */
574
        function compress(message)
575
        {
576
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
577
        }
578
579
        /**
580
         * decompress a message compressed with cryptToolcompress()
581
         *
582
         * @name   CryptTool.decompress
583
         * @function
584
         * @private
585
         * @param  {string} data - base64 data
586
         * @return {string} message
587
         */
588
        function decompress(data)
589
        {
590
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
591
        }
592
593
        /**
594
         * compress, then encrypt message with given key and password
595
         *
596
         * @name   CryptTool.cipher
597
         * @function
598
         * @param  {string} key
599
         * @param  {string} password
600
         * @param  {string} message
601
         * @return {string} data - JSON with encrypted data
602
         */
603
        me.cipher = function(key, password, message)
604
        {
605
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
606
            var options = {
607
                mode: 'gcm',
608
                ks: 256,
609
                ts: 128
610
            };
611
612
            if ((password || '').trim().length === 0) {
613
                return sjcl.encrypt(key, compress(message), options);
614
            }
615
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
616
        }
617
618
        /**
619
         * decrypt message with key, then decompress
620
         *
621
         * @name   CryptTool.decipher
622
         * @function
623
         * @param  {string} key
624
         * @param  {string} password
625
         * @param  {string} data - JSON with encrypted data
626
         * @return {string} decrypted message
627
         */
628
        me.decipher = function(key, password, data)
629
        {
630
            if (data !== undefined) {
631
                try {
632
                    return decompress(sjcl.decrypt(key, data));
633
                } catch(err) {
634
                    try {
635
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
636
                    } catch(e) {
637
                        // ignore error, because ????? @TODO
638
                    }
639
                }
640
            }
641
            return '';
642
        }
643
644
        /**
645
         * checks whether the crypt tool has collected enough entropy
646
         *
647
         * @name   CryptTool.isEntropyReady
648
         * @function
649
         * @return {bool}
650
         */
651
        me.isEntropyReady = function()
652
        {
653
            return sjcl.random.isReady();
654
        }
655
656
        /**
657
         * add a listener function, triggered when enough entropy is available
658
         *
659
         * @name   CryptTool.addEntropySeedListener
660
         * @function
661
         * @param {function} func
662
         */
663
        me.addEntropySeedListener = function(func)
664
        {
665
            sjcl.random.addEventListener('seeded', func);
666
        }
667
668
        /**
669
         * returns a random symmetric key
670
         *
671
         * @name   CryptTool.getSymmetricKey
672
         * @function
673
         * @return {string} func
674
         */
675
        me.getSymmetricKey = function()
676
        {
677
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
678
        }
679
680
        return me;
681
    })();
682
683
    /**
684
     * (Model) Data source (aka MVC)
685
     *
686
     * @name   Model
687
     * @class
688
     */
689
    var Model = (function () {
690
        var me = {};
691
692
        var $cipherData,
693
            $templates;
694
695
        var id = null, symmetricKey = null;
696
697
        /**
698
         * returns the expiration set in the HTML
699
         *
700
         * @name   Model.getExpirationDefault
701
         * @function
702
         * @return string
703
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
704
         */
705
        me.getExpirationDefault = function()
706
        {
707
            return $('#pasteExpiration').val();
708
        }
709
710
        /**
711
         * returns the format set in the HTML
712
         *
713
         * @name   Model.getFormatDefault
714
         * @function
715
         * @return string
716
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
717
         */
718
        me.getFormatDefault = function()
719
        {
720
            return $('#pasteFormatter').val();
721
        }
722
723
        /**
724
         * check if cipher data was supplied
725
         *
726
         * @name   Model.getCipherData
727
         * @function
728
         * @return boolean
729
         */
730
        me.hasCipherData = function()
731
        {
732
            return (me.getCipherData().length > 0);
733
        }
734
735
        /**
736
         * returns the cipher data
737
         *
738
         * @name   Model.getCipherData
739
         * @function
740
         * @return string
741
         */
742
        me.getCipherData = function()
743
        {
744
            return $cipherData.text();
745
        }
746
747
        /**
748
         * get the pastes unique identifier from the URL,
749
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
750
         *
751
         * @name   Model.getPasteId
752
         * @function
753
         * @return {string} unique identifier
754
         * @throws {string}
755
         */
756
        me.getPasteId = function()
757
        {
758
            if (id === null) {
759
                id = window.location.search.substring(1);
760
761
                if (id === '') {
762
                    throw 'no paste id given';
763
                }
764
            }
765
766
            return id;
767
        }
768
769
        /**
770
         * return the deciphering key stored in anchor part of the URL
771
         *
772
         * @name   Model.getPasteKey
773
         * @function
774
         * @return {string|null} key
775
         * @throws {string}
776
         */
777
        me.getPasteKey = function()
778
        {
779
            if (symmetricKey === null) {
780
                symmetricKey = window.location.hash.substring(1);
781
782
                if (symmetricKey === '') {
783
                    throw 'no encryption key given';
784
                }
785
786
                // Some web 2.0 services and redirectors add data AFTER the anchor
787
                // (such as &utm_source=...). We will strip any additional data.
788
                var ampersandPos = symmetricKey.indexOf('&');
789
                if (ampersandPos > -1)
790
                {
791
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
792
                }
793
            }
794
795
            return symmetricKey;
796
        }
797
798
        /**
799
         * returns a jQuery copy of the HTML template
800
         *
801
         * @name Model.getTemplate
802
         * @function
803
         * @param  {string} name - the name of the template
804
         * @return {jQuery}
805
         */
806
        me.getTemplate = function(name)
807
        {
808
            // find template
809
            var $element = $templates.find('#' + name + 'template').clone(true);
810
            // change ID to avoid collisions (one ID should really be unique)
811
            return $element.prop('id', name);
812
        }
813
814
        /**
815
         * resets state, used for unit testing
816
         *
817
         * @name   Model.reset
818
         * @function
819
         */
820
        me.reset = function()
821
        {
822
            $cipherData = $templates = id = symmetricKey = null;
823
        }
824
825
        /**
826
         * init navigation manager
827
         *
828
         * preloads jQuery elements
829
         *
830
         * @name   Model.init
831
         * @function
832
         */
833
        me.init = function()
834
        {
835
            $cipherData = $('#cipherdata');
836
            $templates = $('#templates');
837
        }
838
839
        return me;
840
    })();
841
842
    /**
843
     * Helper functions for user interface
844
     *
845
     * everything directly UI-related, which fits nowhere else
846
     *
847
     * @name   UiHelper
848
     * @param  {object} window
849
     * @param  {object} document
850
     * @class
851
     */
852
    var UiHelper = (function (window, document) {
853
        var me = {};
854
855
        /**
856
         * handle history (pop) state changes
857
         *
858
         * currently this does only handle redirects to the home page.
859
         *
860
         * @name   UiHelper.historyChange
861
         * @private
862
         * @function
863
         * @param  {Event} event
864
         */
865
        function historyChange(event)
866
        {
867
            var currentLocation = Helper.baseUri();
868
            if (event.originalEvent.state === null && // no state object passed
869
                event.originalEvent.target.location.href === currentLocation && // target location is home page
870
                window.location.href === currentLocation // and we are not already on the home page
871
            ) {
872
                // redirect to home page
873
                window.location.href = currentLocation;
874
            }
875
        }
876
877
        /**
878
         * reload the page
879
         *
880
         * This takes the user to the PrivateBin homepage.
881
         *
882
         * @name   UiHelper.reloadHome
883
         * @function
884
         */
885
        me.reloadHome = function()
886
        {
887
            window.location.href = Helper.baseUri();
888
        }
889
890
        /**
891
         * checks whether the element is currently visible in the viewport (so
892
         * the user can actually see it)
893
         *
894
         * @see    {@link https://stackoverflow.com/a/40658647}
895
         * @name   UiHelper.isVisible
896
         * @function
897
         * @param  {jQuery} $element The link hash to move to.
898
         */
899
        me.isVisible = function($element)
900
        {
901
            var elementTop = $element.offset().top;
902
            var viewportTop = $(window).scrollTop();
903
            var viewportBottom = viewportTop + $(window).height();
904
905
            return (elementTop > viewportTop && elementTop < viewportBottom);
906
        }
907
908
        /**
909
         * scrolls to a specific element
910
         *
911
         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
912
         * @name   UiHelper.scrollTo
913
         * @function
914
         * @param  {jQuery}           $element        The link hash to move to.
915
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
916
         * @param  {string}           animationEffect   passed to jQuery .animate
917
         * @param  {function}         finishedCallback  function to call after animation finished
918
         */
919
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
920
        {
921
            var $body = $('html, body'),
922
                margin = 50,
923
                callbackCalled = false;
924
925
            //calculate destination place
926
            var dest = 0;
927
            // if it would scroll out of the screen at the bottom only scroll it as
928
            // far as the screen can go
929
            if ($element.offset().top > $(document).height() - $(window).height()) {
930
                dest = $(document).height() - $(window).height();
931
            } else {
932
                dest = $element.offset().top - margin;
933
            }
934
            // skip animation if duration is set to 0
935
            if (animationDuration === 0) {
936
                window.scrollTo(0, dest);
937
            } else {
938
                // stop previous animation
939
                $body.stop();
940
                // scroll to destination
941
                $body.animate({
942
                    scrollTop: dest
943
                }, animationDuration, animationEffect);
944
            }
945
946
            // as we have finished we can enable scrolling again
947
            $body.queue(function (next) {
948
                if (!callbackCalled) {
949
                    // call user function if needed
950
                    if (typeof finishedCallback !== 'undefined') {
951
                        finishedCallback();
952
                    }
953
954
                    // prevent calling this function twice
955
                    callbackCalled = true;
956
                }
957
                next();
958
            });
959
        }
960
961
        /**
962
         * initialize
963
         *
964
         * @name   UiHelper.init
965
         * @function
966
         */
967
        me.init = function()
968
        {
969
            // update link to home page
970
            $('.reloadlink').prop('href', Helper.baseUri());
971
972
            $(window).on('popstate', historyChange);
973
        }
974
975
        return me;
976
    })(window, document);
977
978
    /**
979
     * Alert/error manager
980
     *
981
     * @name   Alert
982
     * @class
983
     */
984
    var Alert = (function () {
985
        var me = {};
986
987
        var $errorMessage,
988
            $loadingIndicator,
989
            $statusMessage,
990
            $remainingTime;
991
992
        var currentIcon = [
993
            'glyphicon-time', // loading icon
994
            'glyphicon-info-sign', // status icon
995
            '', // resevered for warning, not used yet
996
            'glyphicon-alert' // error icon
997
        ];
998
999
        var alertType = [
1000
            'loading', // not in bootstrap, but using a good value here
1001
            'info', // status icon
1002
            'warning', // not used yet
1003
            'danger' // error icon
1004
        ];
1005
1006
        var customHandler;
1007
1008
        /**
1009
         * forwards a request to the i18n module and shows the element
1010
         *
1011
         * @name   Alert.handleNotification
1012
         * @private
1013
         * @function
1014
         * @param  {int} id - id of notification
1015
         * @param  {jQuery} $element - jQuery object
1016
         * @param  {string|array} args
1017
         * @param  {string|null} icon - optional, icon
1018
         */
1019
        function handleNotification(id, $element, args, icon)
1020
        {
1021
            // basic parsing/conversion of parameters
1022
            if (typeof icon === 'undefined') {
1023
                icon = null;
1024
            }
1025
            if (typeof args === 'undefined') {
1026
                args = null;
1027
            } else if (typeof args === 'string') {
1028
                // convert string to array if needed
1029
                args = [args];
1030
            }
1031
1032
            // pass to custom handler if defined
1033
            if (typeof customHandler === 'function') {
1034
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1035
                if (handlerResult === true) {
1036
                    // if it returs true, skip own handler
1037
                    return;
1038
                }
1039
                if (handlerResult instanceof jQuery) {
1040
                    // continue processing with new element
1041
                    $element = handlerResult;
1042
                    icon = null; // icons not supported in this case
1043
                }
1044
            }
1045
1046
            // handle icon
1047
            if (icon !== null && // icon was passed
1048
                icon !== currentIcon[id] // and it differs from current icon
1049
            ) {
1050
                var $glyphIcon = $element.find(':first');
1051
1052
                // remove (previous) icon
1053
                $glyphIcon.removeClass(currentIcon[id]);
1054
1055
                // any other thing as a string (e.g. 'null') (only) removes the icon
1056
                if (typeof icon === 'string') {
1057
                    // set new icon
1058
                    currentIcon[id] = 'glyphicon-' + icon;
1059
                    $glyphIcon.addClass(currentIcon[id]);
1060
                }
1061
            }
1062
1063
            // show text
1064
            if (args !== null) {
1065
                // add jQuery object to it as first parameter
1066
                args.unshift($element);
1067
                // pass it to I18n
1068
                I18n._.apply(this, args);
1069
            }
1070
1071
            // show notification
1072
            $element.removeClass('hidden');
1073
        }
1074
1075
        /**
1076
         * display a status message
1077
         *
1078
         * This automatically passes the text to I18n for translation.
1079
         *
1080
         * @name   Alert.showStatus
1081
         * @function
1082
         * @param  {string|array} message     string, use an array for %s/%d options
1083
         * @param  {string|null}  icon        optional, the icon to show,
1084
         *                                    default: leave previous icon
1085
         * @param  {bool}         dismissable optional, whether the notification
1086
         *                                    can be dismissed (closed), default: false
1087
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1088
         *                                    notification should be hidden automatically;
1089
         *                                    default: disabled (0); use true for default value
1090
         */
1091
        me.showStatus = function(message, icon, dismissable, autoclose)
0 ignored issues
show
The parameter autoclose is not used and could be removed.

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

Loading history...
The parameter dismissable is not used and could be removed.

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

Loading history...
1092
        {
1093
            console.log('status shown: ', message);
0 ignored issues
show
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1094
            // @TODO: implement dismissable
1095
            // @TODO: implement autoclose
1096
1097
            handleNotification(1, $statusMessage, message, icon);
1098
        }
1099
1100
        /**
1101
         * display an error message
1102
         *
1103
         * This automatically passes the text to I18n for translation.
1104
         *
1105
         * @name   Alert.showError
1106
         * @function
1107
         * @param  {string|array} message     string, use an array for %s/%d options
1108
         * @param  {string|null}  icon        optional, the icon to show, default:
1109
         *                                    leave previous icon
1110
         * @param  {bool}         dismissable optional, whether the notification
1111
         *                                    can be dismissed (closed), default: false
1112
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1113
         *                                    notification should be hidden automatically;
1114
         *                                    default: disabled (0); use true for default value
1115
         */
1116
        me.showError = function(message, icon, dismissable, autoclose)
0 ignored issues
show
The parameter dismissable is not used and could be removed.

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

Loading history...
The parameter autoclose is not used and could be removed.

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

Loading history...
1117
        {
1118
            console.error('error message shown: ', message);
1119
            // @TODO: implement dismissable (bootstrap add-on has it)
1120
            // @TODO: implement autoclose
1121
1122
            handleNotification(3, $errorMessage, message, icon);
1123
        }
1124
1125
        /**
1126
         * display remaining message
1127
         *
1128
         * This automatically passes the text to I18n for translation.
1129
         *
1130
         * @name   Alert.showRemaining
1131
         * @function
1132
         * @param  {string|array} message     string, use an array for %s/%d options
1133
         */
1134
        me.showRemaining = function(message)
1135
        {
1136
            console.log('remaining message shown: ', message);
0 ignored issues
show
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1137
            handleNotification(1, $remainingTime, message);
1138
        }
1139
1140
        /**
1141
         * shows a loading message, optionally with a percentage
1142
         *
1143
         * This automatically passes all texts to the i10s module.
1144
         *
1145
         * @name   Alert.showLoading
1146
         * @function
1147
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1148
         * @param  {int}               percentage   optional, default: null
1149
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1150
         */
1151
        me.showLoading = function(message, percentage, icon)
1152
        {
1153
            if (typeof message !== 'undefined' && message !== null) {
1154
                console.log('status changed: ', message);
0 ignored issues
show
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1155
            }
1156
1157
            // default message text
1158
            if (typeof message === 'undefined') {
1159
                message = 'Loading…';
1160
            }
1161
1162
            // currently percentage parameter is ignored
1163
            // // @TODO handle it here…
1164
1165
            handleNotification(0, $loadingIndicator, message, icon);
1166
1167
            // show loading status (cursor)
1168
            $('body').addClass('loading');
1169
        }
1170
1171
        /**
1172
         * hides the loading message
1173
         *
1174
         * @name   Alert.hideLoading
1175
         * @function
1176
         */
1177
        me.hideLoading = function()
1178
        {
1179
            $loadingIndicator.addClass('hidden');
1180
1181
            // hide loading cursor
1182
            $('body').removeClass('loading');
1183
        }
1184
1185
        /**
1186
         * hides any status/error messages
1187
         *
1188
         * This does not include the loading message.
1189
         *
1190
         * @name   Alert.hideMessages
1191
         * @function
1192
         */
1193
        me.hideMessages = function()
1194
        {
1195
            // also possible: $('.statusmessage').addClass('hidden');
1196
            $statusMessage.addClass('hidden');
1197
            $errorMessage.addClass('hidden');
1198
        }
1199
1200
        /**
1201
         * set a custom handler, which gets all notifications.
1202
         *
1203
         * This handler gets the following arguments:
1204
         * alertType (see array), $element, args, icon
1205
         * If it returns true, the own processing will be stopped so the message
1206
         * will not be displayed. Otherwise it will continue.
1207
         * As an aditional feature it can return q jQuery element, which will
1208
         * then be used to add the message there. Icons are not supported in
1209
         * that case and will be ignored.
1210
         * Pass 'null' to reset/delete the custom handler.
1211
         * Note that there is no notification when a message is supposed to get
1212
         * hidden.
1213
         *
1214
         * @name   Alert.setCustomHandler
1215
         * @function
1216
         * @param {function|null} newHandler
1217
         */
1218
        me.setCustomHandler = function(newHandler)
1219
        {
1220
            customHandler = newHandler;
1221
        }
1222
1223
        /**
1224
         * init status manager
1225
         *
1226
         * preloads jQuery elements
1227
         *
1228
         * @name   Alert.init
1229
         * @function
1230
         */
1231
        me.init = function()
1232
        {
1233
            // hide "no javascript" error message
1234
            $('#noscript').hide();
1235
1236
            // not a reset, but first set of the elements
1237
            $errorMessage = $('#errormessage');
1238
            $loadingIndicator = $('#loadingindicator');
1239
            $statusMessage = $('#status');
1240
            $remainingTime = $('#remainingtime');
1241
        }
1242
1243
        return me;
1244
    })();
1245
1246
    /**
1247
     * handles paste status/result
1248
     *
1249
     * @name   PasteStatus
1250
     * @param  {object} window
1251
     * @class
1252
     */
1253
    var PasteStatus = (function (window) {
1254
        var me = {};
1255
1256
        var $pasteSuccess,
1257
            $pasteUrl,
1258
            $remainingTime,
1259
            $shortenButton;
1260
1261
        /**
1262
         * forward to URL shortener
1263
         *
1264
         * @name   PasteStatus.sendToShortener
1265
         * @private
1266
         * @function
1267
         * @param  {Event} event
1268
         */
1269
        function sendToShortener(event)
0 ignored issues
show
The parameter event is not used and could be removed.

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

Loading history...
1270
        {
1271
            window.location.href = $shortenButton.data('shortener')
1272
                                   + encodeURIComponent($pasteUrl.attr('href'));
1273
        }
1274
1275
        /**
1276
         * Forces opening the paste if the link does not do this automatically.
1277
         *
1278
         * This is necessary as browsers will not reload the page when it is
1279
         * already loaded (which is fake as it is set via history.pushState()).
1280
         *
1281
         * @name   PasteStatus.pasteLinkClick
1282
         * @function
1283
         * @param  {Event} event
1284
         */
1285
        function pasteLinkClick(event)
0 ignored issues
show
The parameter event is not used and could be removed.

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

Loading history...
1286
        {
1287
            // check if location is (already) shown in URL bar
1288
            if (window.location.href === $pasteUrl.attr('href')) {
1289
                // if so we need to load link by reloading the current site
1290
                window.location.reload(true);
1291
            }
1292
        }
1293
1294
        /**
1295
         * creates a notification after a successfull paste upload
1296
         *
1297
         * @name   PasteStatus.createPasteNotification
1298
         * @function
1299
         * @param  {string} url
1300
         * @param  {string} deleteUrl
1301
         */
1302
        me.createPasteNotification = function(url, deleteUrl)
1303
        {
1304
            $('#pastelink').html(
1305
                I18n._(
1306
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1307
                    url, url
1308
                )
1309
            );
1310
            // save newly created element
1311
            $pasteUrl = $('#pasteurl');
1312
            // and add click event
1313
            $pasteUrl.click(pasteLinkClick);
1314
1315
            // shorten button
1316
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1317
1318
            // show result
1319
            $pasteSuccess.removeClass('hidden');
1320
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1321
            Helper.selectText($pasteUrl[0]);
1322
        }
1323
1324
        /**
1325
         * shows the remaining time
1326
         *
1327
         * @name PasteStatus.showRemainingTime
1328
         * @function
1329
         * @param {object} pasteMetaData
1330
         */
1331
        me.showRemainingTime = function(pasteMetaData)
1332
        {
1333
            if (pasteMetaData.burnafterreading) {
1334
                // display paste "for your eyes only" if it is deleted
1335
1336
                // actually remove paste, before we claim it is deleted
1337
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1338
1339
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1340
                $remainingTime.addClass('foryoureyesonly');
1341
1342
                // discourage cloning (it cannot really be prevented)
1343
                TopNav.hideCloneButton();
1344
1345
            } else if (pasteMetaData.expire_date) {
1346
                // display paste expiration
1347
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1348
                    expirationLabel = [
1349
                        'This document will expire in %d ' + expiration[1] + '.',
1350
                        'This document will expire in %d ' + expiration[1] + 's.'
1351
                    ];
1352
1353
                Alert.showRemaining([expirationLabel, expiration[0]]);
1354
                $remainingTime.removeClass('foryoureyesonly')
1355
            } else {
1356
                // never expires
1357
                return;
1358
            }
1359
1360
            // in the end, display notification
1361
            $remainingTime.removeClass('hidden');
1362
        }
1363
1364
        /**
1365
         * hides the remaining time and successful upload notification
1366
         *
1367
         * @name PasteStatus.hideRemainingTime
1368
         * @function
1369
         */
1370
        me.hideMessages = function()
1371
        {
1372
            $remainingTime.addClass('hidden');
1373
            $pasteSuccess.addClass('hidden');
1374
        }
1375
1376
        /**
1377
         * init status manager
1378
         *
1379
         * preloads jQuery elements
1380
         *
1381
         * @name   PasteStatus.init
1382
         * @function
1383
         */
1384
        me.init = function()
1385
        {
1386
            $pasteSuccess = $('#pasteSuccess');
1387
            // $pasteUrl is saved in me.createPasteNotification() after creation
1388
            $remainingTime = $('#remainingtime');
1389
            $shortenButton = $('#shortenbutton');
1390
1391
            // bind elements
1392
            $shortenButton.click(sendToShortener);
1393
        }
1394
1395
        return me;
1396
    })(window);
1397
1398
    /**
1399
     * password prompt
1400
     *
1401
     * @name Prompt
1402
     * @class
1403
     */
1404
    var Prompt = (function () {
1405
        var me = {};
1406
1407
        var $passwordDecrypt,
1408
            $passwordForm,
1409
            $passwordModal;
1410
1411
        var password = '';
1412
1413
        /**
1414
         * submit a password in the modal dialog
1415
         *
1416
         * @name Prompt.submitPasswordModal
1417
         * @private
1418
         * @function
1419
         * @param  {Event} event
1420
         */
1421
        function submitPasswordModal(event)
1422
        {
1423
            event.preventDefault();
1424
1425
            // get input
1426
            password = $passwordDecrypt.val();
1427
1428
            // hide modal
1429
            $passwordModal.modal('hide');
1430
1431
            PasteDecrypter.run();
1432
        }
1433
1434
        /**
1435
         * ask the user for the password and set it
1436
         *
1437
         * @name Prompt.requestPassword
1438
         * @function
1439
         */
1440
        me.requestPassword = function()
1441
        {
1442
            // show new bootstrap method (if available)
1443
            if ($passwordModal.length !== 0) {
1444
                $passwordModal.modal({
1445
                    backdrop: 'static',
1446
                    keyboard: false
1447
                });
1448
                return;
1449
            }
1450
1451
            // fallback to old method for page template
1452
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1453
            if (newPassword === null) {
1454
                throw 'password prompt canceled';
1455
            }
1456
            if (password.length === 0) {
1457
                // recursive…
1458
                return me.requestPassword();
1459
            }
1460
1461
            password = newPassword;
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1462
        }
1463
1464
        /**
1465
         * getthe cached password
1466
         *
1467
         * If you do not get a password with this function
1468
         * (returns an empty string), use requestPassword.
1469
         *
1470
         * @name   Prompt.getPassword
1471
         * @function
1472
         * @return {string}
1473
         */
1474
        me.getPassword = function()
1475
        {
1476
            return password;
1477
        }
1478
1479
        /**
1480
         * init status manager
1481
         *
1482
         * preloads jQuery elements
1483
         *
1484
         * @name   Prompt.init
1485
         * @function
1486
         */
1487
        me.init = function()
1488
        {
1489
            $passwordDecrypt = $('#passworddecrypt');
1490
            $passwordForm = $('#passwordform');
1491
            $passwordModal = $('#passwordmodal');
1492
1493
            // bind events
1494
1495
            // focus password input when it is shown
1496
            $passwordModal.on('shown.bs.Model', function () {
1497
                $passwordDecrypt.focus();
1498
            });
1499
            // handle Model password submission
1500
            $passwordForm.submit(submitPasswordModal);
1501
        }
1502
1503
        return me;
1504
    })();
1505
1506
    /**
1507
     * Manage paste/message input, and preview tab
1508
     *
1509
     * Note that the actual preview is handled by PasteViewer.
1510
     *
1511
     * @name   Editor
1512
     * @class
1513
     */
1514
    var Editor = (function () {
1515
        var me = {};
1516
1517
        var $editorTabs,
1518
            $messageEdit,
1519
            $messagePreview,
1520
            $message;
1521
1522
        var isPreview = false;
1523
1524
        /**
1525
         * support input of tab character
1526
         *
1527
         * @name   Editor.supportTabs
1528
         * @function
1529
         * @param  {Event} event
1530
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1531
         */
1532
        function supportTabs(event)
1533
        {
1534
            var keyCode = event.keyCode || event.which;
1535
            // tab was pressed
1536
            if (keyCode === 9) {
1537
                // get caret position & selection
1538
                var val   = this.value,
1539
                    start = this.selectionStart,
1540
                    end   = this.selectionEnd;
1541
                // set textarea value to: text before caret + tab + text after caret
1542
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1543
                // put caret at right position again
1544
                this.selectionStart = this.selectionEnd = start + 1;
1545
                // prevent the textarea to lose focus
1546
                event.preventDefault();
1547
            }
1548
        }
1549
1550
        /**
1551
         * view the Editor tab
1552
         *
1553
         * @name   Editor.viewEditor
1554
         * @function
1555
         * @param  {Event} event - optional
1556
         */
1557
        function viewEditor(event)
1558
        {
1559
            // toggle buttons
1560
            $messageEdit.addClass('active');
1561
            $messagePreview.removeClass('active');
1562
1563
            PasteViewer.hide();
1564
1565
            // reshow input
1566
            $message.removeClass('hidden');
1567
1568
            me.focusInput();
1569
1570
            // finish
1571
            isPreview = false;
1572
1573
            // prevent jumping of page to top
1574
            if (typeof event !== 'undefined') {
1575
                event.preventDefault();
1576
            }
1577
        }
1578
1579
        /**
1580
         * view the preview tab
1581
         *
1582
         * @name   Editor.viewPreview
1583
         * @function
1584
         * @param  {Event} event
1585
         */
1586
        function viewPreview(event)
1587
        {
1588
            // toggle buttons
1589
            $messageEdit.removeClass('active');
1590
            $messagePreview.addClass('active');
1591
1592
            // hide input as now preview is shown
1593
            $message.addClass('hidden');
1594
1595
            // show preview
1596
            PasteViewer.setText($message.val());
1597
            PasteViewer.run();
1598
1599
            // finish
1600
            isPreview = true;
1601
1602
            // prevent jumping of page to top
1603
            if (typeof event !== 'undefined') {
1604
                event.preventDefault();
1605
            }
1606
        }
1607
1608
        /**
1609
         * get the state of the preview
1610
         *
1611
         * @name   Editor.isPreview
1612
         * @function
1613
         */
1614
        me.isPreview = function()
1615
        {
1616
            return isPreview;
1617
        }
1618
1619
        /**
1620
         * reset the Editor view
1621
         *
1622
         * @name   Editor.resetInput
1623
         * @function
1624
         */
1625
        me.resetInput = function()
1626
        {
1627
            // go back to input
1628
            if (isPreview) {
1629
                viewEditor();
1630
            }
1631
1632
            // clear content
1633
            $message.val('');
1634
        }
1635
1636
        /**
1637
         * shows the Editor
1638
         *
1639
         * @name   Editor.show
1640
         * @function
1641
         */
1642
        me.show = function()
1643
        {
1644
            $message.removeClass('hidden');
1645
            $editorTabs.removeClass('hidden');
1646
        }
1647
1648
        /**
1649
         * hides the Editor
1650
         *
1651
         * @name   Editor.reset
1652
         * @function
1653
         */
1654
        me.hide = function()
1655
        {
1656
            $message.addClass('hidden');
1657
            $editorTabs.addClass('hidden');
1658
        }
1659
1660
        /**
1661
         * focuses the message input
1662
         *
1663
         * @name   Editor.focusInput
1664
         * @function
1665
         */
1666
        me.focusInput = function()
1667
        {
1668
            $message.focus();
1669
        }
1670
1671
        /**
1672
         * sets a new text
1673
         *
1674
         * @name   Editor.setText
1675
         * @function
1676
         * @param {string} newText
1677
         */
1678
        me.setText = function(newText)
1679
        {
1680
            $message.val(newText);
1681
        }
1682
1683
        /**
1684
         * returns the current text
1685
         *
1686
         * @name   Editor.getText
1687
         * @function
1688
         * @return {string}
1689
         */
1690
        me.getText = function()
1691
        {
1692
            return $message.val()
1693
        }
1694
1695
        /**
1696
         * init status manager
1697
         *
1698
         * preloads jQuery elements
1699
         *
1700
         * @name   Editor.init
1701
         * @function
1702
         */
1703
        me.init = function()
1704
        {
1705
            $editorTabs = $('#editorTabs');
1706
            $message = $('#message');
1707
1708
            // bind events
1709
            $message.keydown(supportTabs);
1710
1711
            // bind click events to tab switchers (a), but save parent of them
1712
            // (li)
1713
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1714
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1715
        }
1716
1717
        return me;
1718
    })();
1719
1720
    /**
1721
     * (view) Parse and show paste.
1722
     *
1723
     * @name   PasteViewer
1724
     * @class
1725
     */
1726
    var PasteViewer = (function () {
1727
        var me = {};
1728
1729
        var $placeholder,
1730
            $prettyMessage,
1731
            $prettyPrint,
1732
            $plainText;
1733
1734
        var text,
1735
            format = 'plaintext',
1736
            isDisplayed = false,
1737
            isChanged = true; // by default true as nothing was parsed yet
1738
1739
        /**
1740
         * apply the set format on paste and displays it
1741
         *
1742
         * @name   PasteViewer.parsePaste
1743
         * @private
1744
         * @function
1745
         */
1746
        function parsePaste()
1747
        {
1748
            // skip parsing if no text is given
1749
            if (text === '') {
1750
                return;
1751
            }
1752
1753
            // set text
1754
            Helper.setElementText($plainText, text);
1755
            Helper.setElementText($prettyPrint, text);
1756
1757
            switch (format) {
1758
                case 'markdown':
1759
                    var converter = new showdown.Converter({
1760
                        strikethrough: true,
1761
                        tables: true,
1762
                        tablesHeaderId: true
1763
                    });
1764
                    $plainText.html(
1765
                        converter.makeHtml(text)
1766
                    );
1767
                    // add table classes from bootstrap css
1768
                    $plainText.find('table').addClass('table-condensed table-bordered');
1769
                    break;
1770
                case 'syntaxhighlighting':
1771
                    // @TODO is this really needed or is "one" enough?
1772
                    if (typeof prettyPrint === 'function')
1773
                    {
1774
                        prettyPrint();
1775
                    }
1776
1777
                    $prettyPrint.html(
1778
                        prettyPrintOne(
1779
                            Helper.htmlEntities(text), null, true
1780
                        )
1781
                    );
1782
                    // fall through, as the rest is the same
1783
                default: // = 'plaintext'
1784
                    // convert URLs to clickable links
1785
                    Helper.urls2links($plainText);
1786
                    Helper.urls2links($prettyPrint);
1787
1788
                    $prettyPrint.css('white-space', 'pre-wrap');
1789
                    $prettyPrint.css('word-break', 'normal');
1790
                    $prettyPrint.removeClass('prettyprint');
1791
            }
1792
        }
1793
1794
        /**
1795
         * displays the paste
1796
         *
1797
         * @name   PasteViewer.showPaste
1798
         * @private
1799
         * @function
1800
         */
1801
        function showPaste()
1802
        {
1803
            // instead of "nothing" better display a placeholder
1804
            if (text === '') {
1805
                $placeholder.removeClass('hidden')
1806
                return;
1807
            }
1808
            // otherwise hide the placeholder
1809
            $placeholder.addClass('hidden')
1810
1811
            switch (format) {
1812
                case 'markdown':
1813
                    $plainText.removeClass('hidden');
1814
                    $prettyMessage.addClass('hidden');
1815
                    break;
1816
                default:
1817
                    $plainText.addClass('hidden');
1818
                    $prettyMessage.removeClass('hidden');
1819
                    break;
1820
            }
1821
        }
1822
1823
        /**
1824
         * sets the format in which the text is shown
1825
         *
1826
         * @name   PasteViewer.setFormat
1827
         * @function
1828
         * @param {string} newFormat the the new format
1829
         */
1830
        me.setFormat = function(newFormat)
1831
        {
1832
            // skip if there is no update
1833
            if (format === newFormat) {
1834
                return;
1835
            }
1836
1837
            // needs to update display too, if from or to Markdown is switched
1838
            if (format === 'markdown' || newFormat === 'markdown') {
1839
                isDisplayed = false;
1840
            }
1841
1842
            format = newFormat;
1843
            isChanged = true;
1844
        }
1845
1846
        /**
1847
         * returns the current format
1848
         *
1849
         * @name   PasteViewer.getFormat
1850
         * @function
1851
         * @return {string}
1852
         */
1853
        me.getFormat = function()
1854
        {
1855
            return format;
1856
        }
1857
1858
        /**
1859
         * returns whether the current view is pretty printed
1860
         *
1861
         * @name   PasteViewer.isPrettyPrinted
1862
         * @function
1863
         * @return {bool}
1864
         */
1865
        me.isPrettyPrinted = function()
1866
        {
1867
            return $prettyPrint.hasClass('prettyprinted');
1868
        }
1869
1870
        /**
1871
         * sets the text to show
1872
         *
1873
         * @name   PasteViewer.setText
1874
         * @function
1875
         * @param {string} newText the text to show
1876
         */
1877
        me.setText = function(newText)
1878
        {
1879
            if (text !== newText) {
1880
                text = newText;
1881
                isChanged = true;
1882
            }
1883
        }
1884
1885
        /**
1886
         * gets the current cached text
1887
         *
1888
         * @name   PasteViewer.getText
1889
         * @function
1890
         * @return {string}
1891
         */
1892
        me.getText = function()
1893
        {
1894
            return text;
1895
        }
1896
1897
        /**
1898
         * show/update the parsed text (preview)
1899
         *
1900
         * @name   PasteViewer.run
1901
         * @function
1902
         */
1903
        me.run = function()
1904
        {
1905
            if (isChanged) {
1906
                parsePaste();
1907
                isChanged = false;
1908
            }
1909
1910
            if (!isDisplayed) {
1911
                showPaste();
1912
                isDisplayed = true;
1913
            }
1914
        }
1915
1916
        /**
1917
         * hide parsed text (preview)
1918
         *
1919
         * @name   PasteViewer.hide
1920
         * @function
1921
         */
1922
        me.hide = function()
1923
        {
1924
            if (!isDisplayed) {
1925
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1926
            }
1927
1928
            $plainText.addClass('hidden');
1929
            $prettyMessage.addClass('hidden');
1930
            $placeholder.addClass('hidden');
1931
1932
            isDisplayed = false;
1933
        }
1934
1935
        /**
1936
         * init status manager
1937
         *
1938
         * preloads jQuery elements
1939
         *
1940
         * @name   PasteViewer.init
1941
         * @function
1942
         */
1943
        me.init = function()
1944
        {
1945
            $placeholder = $('#placeholder');
1946
            $plainText = $('#plaintext');
1947
            $prettyMessage = $('#prettymessage');
1948
            $prettyPrint = $('#prettyprint');
1949
1950
            // check requirements
1951
            if (typeof prettyPrintOne !== 'function') {
1952
                Alert.showError([
1953
                    'The library %s is not available. This may cause display errors.',
1954
                    'pretty print'
1955
                ]);
1956
            }
1957
            if (typeof showdown !== 'object') {
1958
                Alert.showError([
1959
                    'The library %s is not available. This may cause display errors.',
1960
                    'showdown'
1961
                ]);
1962
            }
1963
1964
            // get default option from template/HTML or fall back to set value
1965
            format = Model.getFormatDefault() || format;
1966
        }
1967
1968
        return me;
1969
    })();
1970
1971
    /**
1972
     * (view) Show attachment and preview if possible
1973
     *
1974
     * @name   AttachmentViewer
1975
     * @param  {object} window
1976
     * @param  {object} document
1977
     * @class
1978
     */
1979
    var AttachmentViewer = (function (window, document) {
1980
        var me = {};
1981
1982
        var $attachmentLink,
1983
            $attachmentPreview,
1984
            $attachment;
1985
1986
        var attachmentHasPreview = false;
1987
1988
        /**
1989
         * sets the attachment but does not yet show it
1990
         *
1991
         * @name   AttachmentViewer.setAttachment
1992
         * @function
1993
         * @param {string} attachmentData - base64-encoded data of file
1994
         * @param {string} fileName - optional, file name
1995
         */
1996
        me.setAttachment = function(attachmentData, fileName)
1997
        {
1998
            var imagePrefix = 'data:image/';
1999
2000
            $attachmentLink.attr('href', attachmentData);
2001
            if (typeof fileName !== 'undefined') {
2002
                $attachmentLink.attr('download', fileName);
2003
            }
2004
2005
            // if the attachment is an image, display it
2006
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2007
                $attachmentPreview.html(
2008
                    $(document.createElement('img'))
2009
                        .attr('src', attachmentData)
2010
                        .attr('class', 'img-thumbnail')
2011
                );
2012
                attachmentHasPreview = true;
2013
            }
2014
        }
2015
2016
        /**
2017
         * displays the attachment
2018
         *
2019
         * @name AttachmentViewer.showAttachment
2020
         * @function
2021
         */
2022
        me.showAttachment = function()
2023
        {
2024
            $attachment.removeClass('hidden');
2025
2026
            if (attachmentHasPreview) {
2027
                $attachmentPreview.removeClass('hidden');
2028
            }
2029
        }
2030
2031
        /**
2032
         * removes the attachment
2033
         *
2034
         * This automatically hides the attachment containers to, to
2035
         * prevent an inconsistent display.
2036
         *
2037
         * @name AttachmentViewer.removeAttachment
2038
         * @function
2039
         */
2040
        me.removeAttachment = function()
2041
        {
2042
            me.hideAttachment();
2043
            me.hideAttachmentPreview();
2044
            $attachmentLink.prop('href', '');
2045
            $attachmentLink.prop('download', '');
2046
            $attachmentPreview.html('');
2047
        }
2048
2049
        /**
2050
         * hides the attachment
2051
         *
2052
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2053
         * for that) nor will it hide the attachment link if it was moved somewhere
2054
         * else (see AttachmentViewer.moveAttachmentTo).
2055
         *
2056
         * @name AttachmentViewer.hideAttachment
2057
         * @function
2058
         */
2059
        me.hideAttachment = function()
2060
        {
2061
            $attachment.addClass('hidden');
2062
        }
2063
2064
        /**
2065
         * hides the attachment preview
2066
         *
2067
         * @name AttachmentViewer.hideAttachmentPreview
2068
         * @function
2069
         */
2070
        me.hideAttachmentPreview = function()
2071
        {
2072
            $attachmentPreview.addClass('hidden');
2073
        }
2074
2075
        /**
2076
         * checks if there is an attachment
2077
         *
2078
         * @name   AttachmentViewer.hasAttachment
2079
         * @function
2080
         */
2081
        me.hasAttachment = function()
2082
        {
2083
            var link = $attachmentLink.prop('href');
2084
            return (typeof link !== 'undefined' && link !== '')
2085
        }
2086
2087
        /**
2088
         * return the attachment
2089
         *
2090
         * @name   AttachmentViewer.getAttachment
2091
         * @function
2092
         * @returns {array}
2093
         */
2094
        me.getAttachment = function()
2095
        {
2096
            return [
2097
                $attachmentLink.prop('href'),
2098
                $attachmentLink.prop('download')
2099
            ];
2100
        }
2101
2102
        /**
2103
         * moves the attachment link to another element
2104
         *
2105
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2106
         *
2107
         * @name   AttachmentViewer.moveAttachmentTo
2108
         * @function
2109
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2110
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2111
         */
2112
        me.moveAttachmentTo = function($element, label)
2113
        {
2114
            // move elemement to new place
2115
            $attachmentLink.appendTo($element);
2116
2117
            // update text
2118
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2119
        }
2120
2121
        /**
2122
         * initiate
2123
         *
2124
         * preloads jQuery elements
2125
         *
2126
         * @name   AttachmentViewer.init
2127
         * @function
2128
         */
2129
        me.init = function()
2130
        {
2131
            $attachment = $('#attachment');
2132
            $attachmentLink = $('#attachment a');
2133
            $attachmentPreview = $('#attachmentPreview');
2134
        }
2135
2136
        return me;
2137
    })(window, document);
2138
2139
    /**
2140
     * (view) Shows discussion thread and handles replies
2141
     *
2142
     * @name   DiscussionViewer
2143
     * @param  {object} window
2144
     * @param  {object} document
2145
     * @class
2146
     */
2147
    var DiscussionViewer = (function (window, document) {
2148
        var me = {};
2149
2150
        var $commentTail,
2151
            $discussion,
2152
            $reply,
2153
            $replyMessage,
2154
            $replyNickname,
2155
            $replyStatus,
2156
            $commentContainer;
2157
2158
        var replyCommentId;
2159
2160
        /**
2161
         * initializes the templates
2162
         *
2163
         * @name   DiscussionViewer.initTemplates
2164
         * @private
2165
         * @function
2166
         */
2167
        function initTemplates()
2168
        {
2169
            $reply = Model.getTemplate('reply');
2170
            $replyMessage = $reply.find('#replymessage');
2171
            $replyNickname = $reply.find('#nickname');
2172
            $replyStatus = $reply.find('#replystatus');
2173
2174
            // cache jQuery elements
2175
            $commentTail = Model.getTemplate('commenttail');
2176
        }
2177
2178
        /**
2179
         * open the comment entry when clicking the "Reply" button of a comment
2180
         *
2181
         * @name   DiscussionViewer.openReply
2182
         * @private
2183
         * @function
2184
         * @param  {Event} event
2185
         */
2186
        function openReply(event)
2187
        {
2188
            var $source = $(event.target);
2189
2190
            // clear input
2191
            $replyMessage.val('');
2192
            $replyNickname.val('');
2193
2194
            // get comment id from source element
2195
            replyCommentId = $source.parent().prop('id').split('_')[1];
2196
2197
            // move to correct position
2198
            $source.after($reply);
2199
2200
            // show
2201
            $reply.removeClass('hidden');
2202
            $replyMessage.focus();
2203
2204
            event.preventDefault();
2205
        }
2206
2207
        /**
2208
         * custom handler for displaying notifications in own status message area
2209
         *
2210
         * @name   DiscussionViewer.handleNotification
2211
         * @function
2212
         * @param  {string} alertType
2213
         * @param  {jQuery} $element
2214
         * @param  {string|array} args
2215
         * @param  {string|null} icon
2216
         * @return {bool|jQuery}
2217
         */
2218
        me.handleNotification = function(alertType, $element, args, icon)
0 ignored issues
show
The parameter icon is not used and could be removed.

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

Loading history...
The parameter args is not used and could be removed.

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

Loading history...
The parameter $element is not used and could be removed.

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

Loading history...
2219
        {
2220
            // ignore loading messages
2221
            if (alertType === 'loading') {
2222
                return false;
2223
            }
2224
2225
            if (alertType === 'danger') {
2226
                $replyStatus.removeClass('alert-info');
2227
                $replyStatus.addClass('alert-danger');
2228
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2229
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2230
            } else {
2231
                $replyStatus.removeClass('alert-danger');
2232
                $replyStatus.addClass('alert-info');
2233
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2234
                $replyStatus.find(':first').addClass('glyphicon-alert');
2235
            }
2236
2237
            return $replyStatus;
2238
        }
2239
2240
        /**
2241
         * adds another comment
2242
         *
2243
         * @name   DiscussionViewer.addComment
2244
         * @function
2245
         * @param {object} comment
2246
         * @param {string} commentText
2247
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2248
         */
2249
        me.addComment = function(comment, commentText, nickname, $place)
2250
        {
2251
            if (typeof $place === 'undefined') {
2252
                // starting point (default value/fallback)
2253
                $place = $commentContainer;
2254
2255
                // if parent comment exists
2256
                var $parentComment = $('#comment_' + comment.parentid);
2257
                if ($parentComment.length) {
2258
                    // use parent as position for noew comment, so it shifted
2259
                    // to the right
2260
                    $place = $parentComment;
2261
                }
2262
            }
2263
            if (commentText === '') {
2264
                commentText = 'comment decryption failed';
2265
            }
2266
2267
            // create new comment based on template
2268
            var $commentEntry = Model.getTemplate('comment');
2269
            $commentEntry.prop('id', 'comment_' + comment.id);
2270
            var $commentEntryData = $commentEntry.find('div.commentdata');
2271
2272
            // set & parse text
2273
            Helper.setElementText($commentEntryData, commentText);
2274
            Helper.urls2links($commentEntryData);
2275
2276
            // set nickname
2277
            if (nickname.length > 0) {
2278
                $commentEntry.find('span.nickname').text(nickname);
2279
            } else {
2280
                $commentEntry.find('span.nickname').html('<i></i>');
2281
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2282
            }
2283
2284
            // set date
2285
            $commentEntry.find('span.commentdate')
2286
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2287
                      .attr('title', 'CommentID: ' + comment.id);
2288
2289
            // if an avatar is available, display it
2290
            if (comment.meta.vizhash) {
2291
                $commentEntry.find('span.nickname')
2292
                             .before(
2293
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2294
                             );
2295
                $(document).on('languageLoaded', function () {
2296
                    $commentEntry.find('img.vizhash')
2297
                                 .prop('title', I18n._('Avatar generated from IP address'));
2298
                });
2299
            }
2300
2301
            // finally append comment
2302
            $place.append($commentEntry);
2303
        }
2304
2305
        /**
2306
         * finishes the discussion area after last comment
2307
         *
2308
         * @name   DiscussionViewer.finishDiscussion
2309
         * @function
2310
         */
2311
        me.finishDiscussion = function()
2312
        {
2313
            // add 'add new comment' area
2314
            $commentContainer.append($commentTail);
2315
2316
            // show discussions
2317
            $discussion.removeClass('hidden');
2318
        }
2319
2320
        /**
2321
         * shows the discussion area
2322
         *
2323
         * @name   DiscussionViewer.showDiscussion
2324
         * @function
2325
         */
2326
        me.showDiscussion = function()
2327
        {
2328
            $discussion.removeClass('hidden');
2329
        }
2330
2331
        /**
2332
         * removes the old discussion and prepares everything for creating a new
2333
         * one.
2334
         *
2335
         * @name   DiscussionViewer.prepareNewDisucssion
2336
         * @function
2337
         */
2338
        me.prepareNewDisucssion = function()
2339
        {
2340
            $commentContainer.html('');
2341
            $discussion.addClass('hidden');
2342
2343
            // (re-)init templates
2344
            initTemplates();
2345
        }
2346
2347
        /**
2348
         * returns the user put into the reply form
2349
         *
2350
         * @name   DiscussionViewer.getReplyData
2351
         * @function
2352
         * @return {array}
2353
         */
2354
        me.getReplyData = function()
2355
        {
2356
            return [
2357
                $replyMessage.val(),
2358
                $replyNickname.val()
2359
            ];
2360
        }
2361
2362
        /**
2363
         * highlights a specific comment and scrolls to it if necessary
2364
         *
2365
         * @name   DiscussionViewer.highlightComment
2366
         * @function
2367
         * @param {string} commentId
2368
         * @param {bool} fadeOut - whether to fade out the comment
2369
         */
2370
        me.highlightComment = function(commentId, fadeOut)
2371
        {
2372
            var $comment = $('#comment_' + commentId);
2373
            // in case comment does not exist, cancel
2374
            if ($comment.length === 0) {
2375
                return;
2376
            }
2377
2378
            var highlightComment = function () {
2379
                $comment.addClass('highlight');
2380
                if (fadeOut === true) {
2381
                    setTimeout(function () {
2382
                        $comment.removeClass('highlight');
2383
                    }, 300);
2384
                }
2385
            }
2386
2387
            if (UiHelper.isVisible($comment)) {
2388
                return highlightComment();
2389
            }
2390
2391
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
2392
        }
2393
2394
        /**
2395
         * returns the id of the parent comment the user is replying to
2396
         *
2397
         * @name   DiscussionViewer.getReplyCommentId
2398
         * @function
2399
         * @return {int|undefined}
2400
         */
2401
        me.getReplyCommentId = function()
2402
        {
2403
            return replyCommentId;
2404
        }
2405
2406
        /**
2407
         * initiate
2408
         *
2409
         * preloads jQuery elements
2410
         *
2411
         * @name   DiscussionViewer.init
2412
         * @function
2413
         */
2414
        me.init = function()
2415
        {
2416
            // bind events to templates (so they are later cloned)
2417
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2418
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2419
2420
            $commentContainer = $('#commentcontainer');
2421
            $discussion = $('#discussion');
2422
        }
2423
2424
        return me;
2425
    })(window, document);
2426
2427
    /**
2428
     * Manage top (navigation) bar
2429
     *
2430
     * @name   TopNav
2431
     * @param  {object} window
2432
     * @param  {object} document
2433
     * @class
2434
     */
2435
    var TopNav = (function (window, document) {
2436
        var me = {};
2437
2438
        var createButtonsDisplayed = false;
2439
        var viewButtonsDisplayed = false;
2440
2441
        var $attach,
2442
            $burnAfterReading,
2443
            $burnAfterReadingOption,
2444
            $cloneButton,
2445
            $customAttachment,
2446
            $expiration,
2447
            $fileRemoveButton,
2448
            $fileWrap,
2449
            $formatter,
2450
            $newButton,
2451
            $openDiscussion,
2452
            $openDiscussionOption,
2453
            $password,
2454
            $passwordInput,
2455
            $rawTextButton,
2456
            $sendButton;
2457
2458
        var pasteExpiration = '1week';
2459
2460
        /**
2461
         * set the expiration on bootstrap templates in dropdown
2462
         *
2463
         * @name   TopNav.updateExpiration
2464
         * @private
2465
         * @function
2466
         * @param  {Event} event
2467
         */
2468
        function updateExpiration(event)
2469
        {
2470
            // get selected option
2471
            var target = $(event.target);
2472
2473
            // update dropdown display and save new expiration time
2474
            $('#pasteExpirationDisplay').text(target.text());
2475
            pasteExpiration = target.data('expiration');
2476
2477
            event.preventDefault();
2478
        }
2479
2480
        /**
2481
         * set the format on bootstrap templates in dropdown
2482
         *
2483
         * @name   TopNav.updateFormat
2484
         * @private
2485
         * @function
2486
         * @param  {Event} event
2487
         */
2488
        function updateFormat(event)
2489
        {
2490
            // get selected option
2491
            var $target = $(event.target);
2492
2493
            // update dropdown display and save new format
2494
            var newFormat = $target.data('format');
2495
            $('#pasteFormatterDisplay').text($target.text());
2496
            PasteViewer.setFormat(newFormat);
2497
2498
            // update preview
2499
            if (Editor.isPreview()) {
2500
                PasteViewer.run();
2501
            }
2502
2503
            event.preventDefault();
2504
        }
2505
2506
        /**
2507
         * when "burn after reading" is checked, disable discussion
2508
         *
2509
         * @name   TopNav.changeBurnAfterReading
2510
         * @private
2511
         * @function
2512
         */
2513
        function changeBurnAfterReading()
2514
        {
2515
            if ($burnAfterReading.is(':checked')) {
2516
                $openDiscussionOption.addClass('buttondisabled');
2517
                $openDiscussion.prop('checked', false);
2518
2519
                // if button is actually disabled, force-enable it and uncheck other button
2520
                $burnAfterReadingOption.removeClass('buttondisabled');
2521
            } else {
2522
                $openDiscussionOption.removeClass('buttondisabled');
2523
            }
2524
        }
2525
2526
        /**
2527
         * when discussion is checked, disable "burn after reading"
2528
         *
2529
         * @name   TopNav.changeOpenDiscussion
2530
         * @private
2531
         * @function
2532
         */
2533
        function changeOpenDiscussion()
2534
        {
2535
            if ($openDiscussion.is(':checked')) {
2536
                $burnAfterReadingOption.addClass('buttondisabled');
2537
                $burnAfterReading.prop('checked', false);
2538
2539
                // if button is actually disabled, force-enable it and uncheck other button
2540
                $openDiscussionOption.removeClass('buttondisabled');
2541
            } else {
2542
                $burnAfterReadingOption.removeClass('buttondisabled');
2543
            }
2544
        }
2545
2546
        /**
2547
         * return raw text
2548
         *
2549
         * @name   TopNav.rawText
2550
         * @private
2551
         * @function
2552
         * @param  {Event} event
2553
         */
2554
        function rawText(event)
0 ignored issues
show
The parameter event is not used and could be removed.

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

Loading history...
2555
        {
2556
            TopNav.hideAllButtons();
2557
            Alert.showLoading('Showing raw text…', 0, 'time');
2558
            var paste = PasteViewer.getText();
2559
2560
            // push a new state to allow back navigation with browser back button
2561
            history.pushState(
2562
                {type: 'raw'},
2563
                document.title,
2564
                // recreate paste URL
2565
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2566
                Model.getPasteKey()
2567
            );
2568
2569
            // we use text/html instead of text/plain to avoid a bug when
2570
            // reloading the raw text view (it reverts to type text/html)
2571
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2572
            var newDoc = document.open('text/html', 'replace');
2573
            newDoc.write('<!DOCTYPE html><html><head>');
2574
            for (var i = 0; i < $head.length; i++) {
2575
                newDoc.write($head[i].outerHTML);
2576
            }
2577
            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
2578
            newDoc.close();
2579
        }
2580
2581
        /**
2582
         * saves the language in a cookie and reloads the page
2583
         *
2584
         * @name   TopNav.setLanguage
2585
         * @private
2586
         * @function
2587
         * @param  {Event} event
2588
         */
2589
        function setLanguage(event)
2590
        {
2591
            document.cookie = 'lang=' + $(event.target).data('lang');
2592
            UiHelper.reloadHome();
2593
        }
2594
2595
        /**
2596
         * hides all messages and creates a new paste
2597
         *
2598
         * @name   TopNav.clickNewPaste
2599
         * @private
2600
         * @function
2601
         * @param  {Event} event
2602
         */
2603
        function clickNewPaste(event)
0 ignored issues
show
The parameter event is not used and could be removed.

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

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

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

Loading history...
3915
        {
3916
            TopNav.collapseBar();
3917
            TopNav.hideAllButtons();
3918
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3919
3920
            // hide messages from previous paste
3921
            me.hideStatusMessages();
3922
3923
            // erase the id and the key in url
3924
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3925
3926
            if (AttachmentViewer.hasAttachment()) {
3927
                AttachmentViewer.moveAttachmentTo(
3928
                    TopNav.getCustomAttachment(),
3929
                    'Cloned: \'%s\''
3930
                );
3931
                TopNav.hideFileSelector();
3932
                AttachmentViewer.hideAttachment();
3933
                // NOTE: it also looks nice without removing the attachment
3934
                // but for a consistent display we remove it…
3935
                AttachmentViewer.hideAttachmentPreview();
3936
                TopNav.showCustomAttachment();
3937
3938
                // show another status message to make the user aware that the
3939
                // file was cloned too!
3940
                Alert.showStatus(
3941
                    [
3942
                        'The cloned file \'%s\' was attached to this paste.',
3943
                        AttachmentViewer.getAttachment()[1]
3944
                    ], 'copy', true, true);
3945
            }
3946
3947
            Editor.setText(PasteViewer.getText())
3948
            PasteViewer.hide();
3949
            Editor.show();
3950
3951
            Alert.hideLoading();
3952
            TopNav.showCreateButtons();
3953
        }
3954
3955
        /**
3956
         * removes a saved paste
3957
         *
3958
         * @name   Controller.removePaste
3959
         * @function
3960
         * @param  {string} pasteId
3961
         * @param  {string} deleteToken
3962
         */
3963
        me.removePaste = function(pasteId, deleteToken) {
3964
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3965
            // so we use a POST request
3966
            Uploader.prepare();
3967
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3968
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3969
3970
            Uploader.setFailure(function () {
3971
                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3972
            })
3973
            Uploader.run();
3974
        }
3975
3976
        /**
3977
         * application start
3978
         *
3979
         * @name   Controller.init
3980
         * @function
3981
         */
3982
        me.init = function()
3983
        {
3984
            // first load translations
3985
            I18n.loadTranslations();
3986
3987
            // initialize other modules/"classes"
3988
            Alert.init();
3989
            Model.init();
3990
3991
            AttachmentViewer.init();
3992
            DiscussionViewer.init();
3993
            Editor.init();
3994
            PasteDecrypter.init();
3995
            PasteEncrypter.init();
3996
            PasteStatus.init();
3997
            PasteViewer.init();
3998
            Prompt.init();
3999
            TopNav.init();
4000
            UiHelper.init();
4001
            Uploader.init();
4002
4003
            // display an existing paste
4004
            if (Model.hasCipherData()) {
4005
                return me.showPaste();
4006
            }
4007
4008
            // otherwise create a new paste
4009
            me.newPaste();
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
4010
        }
4011
4012
        return me;
4013
    })(window, document);
4014
4015
    return {
4016
        Helper: Helper,
4017
        I18n: I18n,
4018
        CryptTool: CryptTool,
4019
        Model: Model,
4020
        UiHelper: UiHelper,
4021
        Alert: Alert,
4022
        PasteStatus: PasteStatus,
4023
        Prompt: Prompt,
4024
        Editor: Editor,
4025
        PasteViewer: PasteViewer,
4026
        AttachmentViewer: AttachmentViewer,
4027
        DiscussionViewer: DiscussionViewer,
4028
        TopNav: TopNav,
4029
        Uploader: Uploader,
4030
        PasteEncrypter: PasteEncrypter,
4031
        PasteDecrypter: PasteDecrypter,
4032
        Controller: Controller
4033
    };
4034
}(jQuery, sjcl, Base64, RawDeflate);
4035