Completed
Push — master ( 9b2af0...bbcc3e )
by El
03:42
created

js/privatebin.js (34 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
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
         * checks if a string is valid text (and not onyl whitespace)
112
         *
113
         * @name Helper.isValidText
114
         * @function
115
         * @param  {string} string
116
         * @return {bool}
117
         */
118
        me.isValidText = function(string)
119
        {
120
            return (string.length > 0 && $.trim(string) !== '')
121
        }
122
123
        /**
124
         * text range selection
125
         *
126
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
127
         * @name   Helper.selectText
128
         * @function
129
         * @param  {HTMLElement} element
130
         */
131
        me.selectText = function(element)
132
        {
133
            var range, selection;
134
135
            // MS
136
            if (document.body.createTextRange) {
137
                range = document.body.createTextRange();
138
                range.moveToElementText(element);
139
                range.select();
140
            } else if (window.getSelection){
141
                selection = window.getSelection();
142
                range = document.createRange();
143
                range.selectNodeContents(element);
144
                selection.removeAllRanges();
145
                selection.addRange(range);
146
            }
147
        }
148
149
        /**
150
         * set text of a jQuery element (required for IE),
151
         *
152
         * @name   Helper.setElementText
153
         * @function
154
         * @param  {jQuery} $element - a jQuery element
155
         * @param  {string} text - the text to enter
156
         */
157
        me.setElementText = function($element, text)
158
        {
159
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
160
            if ($('#oldienotice').is(':visible')) {
161
                var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
162
                $element.html('<pre>' + html + '</pre>');
163
            }
164
            // for other (sane) browsers:
165
            else
166
            {
167
                $element.text(text);
168
            }
169
        }
170
171
        /**
172
         * convert URLs to clickable links.
173
         * URLs to handle:
174
         * <pre>
175
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
176
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
177
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
178
         * </pre>
179
         *
180
         * @name   Helper.urls2links
181
         * @function
182
         * @param  {Object} $element - a jQuery DOM element
183
         */
184
        me.urls2links = function($element)
185
        {
186
            var markup = '<a href="$1" rel="nofollow">$1</a>';
187
            $element.html(
188
                $element.html().replace(
189
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
190
                    markup
191
                )
192
            );
193
            $element.html(
194
                $element.html().replace(
195
                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
196
                    markup
197
                )
198
            );
199
        }
200
201
        /**
202
         * minimal sprintf emulation for %s and %d formats
203
         *
204
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
205
         * @name   Helper.sprintf
206
         * @function
207
         * @param  {string} format
0 ignored issues
show
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
208
         * @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...
209
         * @return {string}
210
         */
211
        me.sprintf = function()
212
        {
213
            var args = Array.prototype.slice.call(arguments);
214
            var format = args[0],
215
                i = 1;
216
            return format.replace(/%((%)|s|d)/g, function (m) {
217
                // m is the matched format, e.g. %s, %d
218
                var val;
219
                if (m[2]) {
220
                    val = m[2];
221
                } else {
222
                    val = args[i];
223
                    // A switch statement so that the formatter can be extended.
224
                    switch (m)
225
                    {
226
                        case '%d':
227
                            val = parseFloat(val);
228
                            if (isNaN(val)) {
229
                                val = 0;
230
                            }
231
                            break;
232
                        default:
233
                            // Default is %s
234
                    }
235
                    ++i;
236
                }
237
                return val;
238
            });
239
        }
240
241
        /**
242
         * get value of cookie, if it was set, empty string otherwise
243
         *
244
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
245
         * @name   Helper.getCookie
246
         * @function
247
         * @param  {string} cname
248
         * @return {string}
249
         */
250
        me.getCookie = function(cname) {
251
            var name = cname + '=',
252
                ca = document.cookie.split(';');
253
            for (var i = 0; i < ca.length; ++i) {
254
                var c = ca[i];
255
                while (c.charAt(0) === ' ')
256
                {
257
                    c = c.substring(1);
258
                }
259
                if (c.indexOf(name) === 0)
260
                {
261
                    return c.substring(name.length, c.length);
262
                }
263
            }
264
            return '';
265
        }
266
267
        /**
268
         * get the current location (without search or hash part of the URL),
269
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
270
         *
271
         * @name   Helper.baseUri
272
         * @function
273
         * @return {string}
274
         */
275
        me.baseUri = function()
276
        {
277
            // check for cached version
278
            if (baseUri !== null) {
279
                return baseUri;
280
            }
281
282
            baseUri = window.location.origin + window.location.pathname;
283
            return baseUri;
284
        }
285
286
        /**
287
         * convert all applicable characters to HTML entities
288
         *
289
         * @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}
290
         * @name   Helper.htmlEntities
291
         * @function
292
         * @param  {string} str
293
         * @return {string} escaped HTML
294
         */
295
        me.htmlEntities = function(str) {
296
            return String(str).replace(
297
                /[&<>"'`=\/]/g, function(s) {
298
                    return entityMap[s];
299
                });
300
        }
301
302
        /**
303
         * resets state, used for unit testing
304
         *
305
         * @name   Helper.reset
306
         * @function
307
         */
308
        me.reset = function()
309
        {
310
            baseUri = null;
311
        }
312
313
        return me;
314
    })();
315
316
    /**
317
     * internationalization module
318
     *
319
     * @name I18n
320
     * @param  {object} window
321
     * @param  {object} document
322
     * @class
323
     */
324
    var I18n = (function (window, document) {
325
        var me = {};
326
327
        /**
328
         * const for string of loaded language
329
         *
330
         * @name I18n.languageLoadedEvent
331
         * @private
332
         * @prop   {string}
333
         * @readonly
334
         */
335
        var languageLoadedEvent = 'languageLoaded';
336
337
        /**
338
         * supported languages, minus the built in 'en'
339
         *
340
         * @name I18n.supportedLanguages
341
         * @private
342
         * @prop   {string[]}
343
         * @readonly
344
         */
345
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
346
347
        /**
348
         * built in language
349
         *
350
         * @name I18n.language
351
         * @private
352
         * @prop   {string|null}
353
         */
354
        var language = null;
355
356
        /**
357
         * translation cache
358
         *
359
         * @name I18n.translations
360
         * @private
361
         * @enum   {Object}
362
         */
363
        var translations = {};
364
365
        /**
366
         * translate a string, alias for I18n.translate
367
         *
368
         * @name   I18n._
369
         * @function
370
         * @param  {jQuery} $element - optional
0 ignored issues
show
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
371
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
372
         * @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...
373
         * @return {string}
374
         */
375
        me._ = function()
376
        {
377
            return me.translate.apply(this, arguments);
378
        }
379
380
        /**
381
         * translate a string
382
         *
383
         * Optionally pass a jQuery element as the first parameter, to automatically
384
         * let the text of this element be replaced. In case the (asynchronously
385
         * loaded) language is not downloadet yet, this will make sure the string
386
         * is replaced when it is actually loaded.
387
         * So for easy translations passing the jQuery object to apply it to is
388
         * more save, especially when they are loaded in the beginning.
389
         *
390
         * @name   I18n.translate
391
         * @function
392
         * @param  {jQuery} $element - optional
0 ignored issues
show
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
393
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
394
         * @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...
395
         * @return {string}
396
         */
397
        me.translate = function()
398
        {
399
            // convert parameters to array
400
            var args = Array.prototype.slice.call(arguments),
401
                messageId,
402
                $element = null;
403
404
            // parse arguments
405
            if (args[0] instanceof jQuery) {
406
                // optional jQuery element as first parameter
407
                $element = args[0];
408
                args.shift();
409
            }
410
411
            // extract messageId from arguments
412
            var usesPlurals = $.isArray(args[0]);
413
            if (usesPlurals) {
414
                // use the first plural form as messageId, otherwise the singular
415
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
416
            } else {
417
                messageId = args[0];
418
            }
419
420
            if (messageId.length === 0) {
421
                return messageId;
422
            }
423
424
            // if no translation string cannot be found (in translations object)
425
            if (!translations.hasOwnProperty(messageId) || language === null) {
426
                // if language is still loading and we have an elemt assigned
427
                if (language === null && $element !== null) {
428
                    // handle the error by attaching the language loaded event
429
                    var orgArguments = arguments;
430
                    $(document).on(languageLoadedEvent, function () {
431
                        // log to show that the previous error could be mitigated
432
                        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...
433
                        // re-execute this function
434
                        me.translate.apply(this, orgArguments);
435
                    });
436
437
                    // and fall back to English for now until the real language
438
                    // file is loaded
439
                }
440
441
                // for all other langauges than English for which this behaviour
442
                // is expected as it is built-in, log error
443
                if (language !== null && language !== 'en') {
444
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
445
                    // fallback to English
446
                }
447
448
                // save English translation (should be the same on both sides)
449
                translations[messageId] = args[0];
450
            }
451
452
            // lookup plural translation
453
            if (usesPlurals && $.isArray(translations[messageId])) {
454
                var n = parseInt(args[1] || 1, 10),
455
                    key = me.getPluralForm(n),
456
                    maxKey = translations[messageId].length - 1;
457
                if (key > maxKey) {
458
                    key = maxKey;
459
                }
460
                args[0] = translations[messageId][key];
461
                args[1] = n;
462
            } else {
463
                // lookup singular translation
464
                args[0] = translations[messageId];
465
            }
466
467
            // format string
468
            var output = Helper.sprintf.apply(this, args);
469
470
            // if $element is given, apply text to element
471
            if ($element !== null) {
472
                // get last text node of element
473
                var content = $element.contents();
474
                if (content.length > 1) {
475
                    content[content.length - 1].nodeValue = ' ' + output;
476
                } else {
477
                    $element.text(output);
478
                }
479
            }
480
481
            return output;
482
        }
483
484
        /**
485
         * per language functions to use to determine the plural form
486
         *
487
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
488
         * @name   I18n.getPluralForm
489
         * @function
490
         * @param  {int} n
491
         * @return {int} array key
492
         */
493
        me.getPluralForm = function(n) {
494
            switch (language)
495
            {
496
                case 'fr':
497
                case 'oc':
498
                case 'zh':
499
                    return (n > 1 ? 1 : 0);
500
                case 'pl':
501
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
502
                case 'ru':
503
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
504
                case 'sl':
505
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
506
                // de, en, es, it, no, pt
507
                default:
508
                    return (n !== 1 ? 1 : 0);
509
            }
510
        }
511
512
        /**
513
         * load translations into cache
514
         *
515
         * @name   I18n.loadTranslations
516
         * @function
517
         */
518
        me.loadTranslations = function()
519
        {
520
            var newLanguage = Helper.getCookie('lang');
521
522
            // auto-select language based on browser settings
523
            if (newLanguage.length === 0) {
524
                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
525
            }
526
527
            // if language is already used skip update
528
            if (newLanguage === language) {
529
                return;
530
            }
531
532
            // if language is built-in (English) skip update
533
            if (newLanguage === 'en') {
534
                language = 'en';
535
                return;
536
            }
537
538
            // if language is not supported, show error
539
            if (supportedLanguages.indexOf(newLanguage) === -1) {
540
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
541
                language = 'en';
542
                return;
543
            }
544
545
            // load strings from JSON
546
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
547
                language = newLanguage;
548
                translations = data;
549
                $(document).triggerHandler(languageLoadedEvent);
550
            }).fail(function (data, textStatus, errorMsg) {
551
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
552
                language = 'en';
553
            });
554
        }
555
556
        return me;
557
    })(window, document);
558
559
    /**
560
     * handles everything related to en/decryption
561
     *
562
     * @name CryptTool
563
     * @class
564
     */
565
    var CryptTool = (function () {
566
        var me = {};
567
568
        /**
569
         * compress a message (deflate compression), returns base64 encoded data
570
         *
571
         * @name   CryptTool.compress
572
         * @function
573
         * @private
574
         * @param  {string} message
575
         * @return {string} base64 data
576
         */
577
        function compress(message)
578
        {
579
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
580
        }
581
582
        /**
583
         * decompress a message compressed with cryptToolcompress()
584
         *
585
         * @name   CryptTool.decompress
586
         * @function
587
         * @private
588
         * @param  {string} data - base64 data
589
         * @return {string} message
590
         */
591
        function decompress(data)
592
        {
593
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
594
        }
595
596
        /**
597
         * compress, then encrypt message with given key and password
598
         *
599
         * @name   CryptTool.cipher
600
         * @function
601
         * @param  {string} key
602
         * @param  {string} password
603
         * @param  {string} message
604
         * @return {string} data - JSON with encrypted data
605
         */
606
        me.cipher = function(key, password, message)
607
        {
608
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
609
            var options = {
610
                mode: 'gcm',
611
                ks: 256,
612
                ts: 128
613
            };
614
615
            if ((password || '').trim().length === 0) {
616
                return sjcl.encrypt(key, compress(message), options);
617
            }
618
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
619
        }
620
621
        /**
622
         * decrypt message with key, then decompress
623
         *
624
         * @name   CryptTool.decipher
625
         * @function
626
         * @param  {string} key
627
         * @param  {string} password
628
         * @param  {string} data - JSON with encrypted data
629
         * @return {string} decrypted message
630
         */
631
        me.decipher = function(key, password, data)
632
        {
633
            if (data !== undefined) {
634
                try {
635
                    return decompress(sjcl.decrypt(key, data));
636
                } catch(err) {
637
                    try {
638
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
639
                    } catch(e) {
640
                        // ignore error, because ????? @TODO
641
                    }
642
                }
643
            }
644
            return '';
645
        }
646
647
        /**
648
         * checks whether the crypt tool has collected enough entropy
649
         *
650
         * @name   CryptTool.isEntropyReady
651
         * @function
652
         * @return {bool}
653
         */
654
        me.isEntropyReady = function()
655
        {
656
            return sjcl.random.isReady();
657
        }
658
659
        /**
660
         * add a listener function, triggered when enough entropy is available
661
         *
662
         * @name   CryptTool.addEntropySeedListener
663
         * @function
664
         * @param {function} func
665
         */
666
        me.addEntropySeedListener = function(func)
667
        {
668
            sjcl.random.addEventListener('seeded', func);
669
        }
670
671
        /**
672
         * returns a random symmetric key
673
         *
674
         * @name   CryptTool.getSymmetricKey
675
         * @function
676
         * @return {string} func
677
         */
678
        me.getSymmetricKey = function()
679
        {
680
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
681
        }
682
683
        return me;
684
    })();
685
686
    /**
687
     * (Model) Data source (aka MVC)
688
     *
689
     * @name   Model
690
     * @class
691
     */
692
    var Model = (function () {
693
        var me = {};
694
695
        var $cipherData,
696
            $templates;
697
698
        var id = null, symmetricKey = null;
699
700
        /**
701
         * returns the expiration set in the HTML
702
         *
703
         * @name   Model.getExpirationDefault
704
         * @function
705
         * @return string
706
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
707
         */
708
        me.getExpirationDefault = function()
709
        {
710
            return $('#pasteExpiration').val();
711
        }
712
713
        /**
714
         * returns the format set in the HTML
715
         *
716
         * @name   Model.getFormatDefault
717
         * @function
718
         * @return string
719
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
720
         */
721
        me.getFormatDefault = function()
722
        {
723
            return $('#pasteFormatter').val();
724
        }
725
726
        /**
727
         * check if cipher data was supplied
728
         *
729
         * @name   Model.getCipherData
730
         * @function
731
         * @return boolean
732
         */
733
        me.hasCipherData = function()
734
        {
735
            return (me.getCipherData().length > 0);
736
        }
737
738
        /**
739
         * returns the cipher data
740
         *
741
         * @name   Model.getCipherData
742
         * @function
743
         * @return string
744
         */
745
        me.getCipherData = function()
746
        {
747
            return $cipherData.text();
748
        }
749
750
        /**
751
         * get the pastes unique identifier from the URL,
752
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
753
         *
754
         * @name   Model.getPasteId
755
         * @function
756
         * @return {string} unique identifier
757
         * @throws {string}
758
         */
759
        me.getPasteId = function()
760
        {
761
            if (id === null) {
762
                id = window.location.search.substring(1);
763
764
                if (id === '') {
765
                    throw 'no paste id given';
766
                }
767
            }
768
769
            return id;
770
        }
771
772
        /**
773
         * return the deciphering key stored in anchor part of the URL
774
         *
775
         * @name   Model.getPasteKey
776
         * @function
777
         * @return {string|null} key
778
         * @throws {string}
779
         */
780
        me.getPasteKey = function()
781
        {
782
            if (symmetricKey === null) {
783
                symmetricKey = window.location.hash.substring(1);
784
785
                if (symmetricKey === '') {
786
                    throw 'no encryption key given';
787
                }
788
789
                // Some web 2.0 services and redirectors add data AFTER the anchor
790
                // (such as &utm_source=...). We will strip any additional data.
791
                var ampersandPos = symmetricKey.indexOf('&');
792
                if (ampersandPos > -1)
793
                {
794
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
795
                }
796
            }
797
798
            return symmetricKey;
799
        }
800
801
        /**
802
         * returns a jQuery copy of the HTML template
803
         *
804
         * @name Model.getTemplate
805
         * @function
806
         * @param  {string} name - the name of the template
807
         * @return {jQuery}
808
         */
809
        me.getTemplate = function(name)
810
        {
811
            // find template
812
            var $element = $templates.find('#' + name + 'template').clone(true);
813
            // change ID to avoid collisions (one ID should really be unique)
814
            return $element.prop('id', name);
815
        }
816
817
        /**
818
         * resets state, used for unit testing
819
         *
820
         * @name   Model.reset
821
         * @function
822
         */
823
        me.reset = function()
824
        {
825
            $cipherData = $templates = id = symmetricKey = null;
826
        }
827
828
829
        /**
830
         * init navigation manager
831
         *
832
         * preloads jQuery elements
833
         *
834
         * @name   Model.init
835
         * @function
836
         */
837
        me.init = function()
838
        {
839
            $cipherData = $('#cipherdata');
840
            $templates = $('#templates');
841
        }
842
843
        return me;
844
    })();
845
846
    /**
847
     * Helper functions for user interface
848
     *
849
     * everything directly UI-related, which fits nowhere else
850
     *
851
     * @name   UiHelper
852
     * @param  {object} window
853
     * @param  {object} document
854
     * @class
855
     */
856
    var UiHelper = (function (window, document) {
857
        var me = {};
858
859
        /**
860
         * handle history (pop) state changes
861
         *
862
         * currently this does only handle redirects to the home page.
863
         *
864
         * @name   UiHelper.historyChange
865
         * @private
866
         * @function
867
         * @param  {Event} event
868
         */
869
        function historyChange(event)
870
        {
871
            var currentLocation = Helper.baseUri();
872
            if (event.originalEvent.state === null && // no state object passed
873
                event.originalEvent.target.location.href === currentLocation && // target location is home page
874
                window.location.href === currentLocation // and we are not already on the home page
875
            ) {
876
                // redirect to home page
877
                window.location.href = currentLocation;
878
            }
879
        }
880
881
        /**
882
         * reload the page
883
         *
884
         * This takes the user to the PrivateBin homepage.
885
         *
886
         * @name   UiHelper.reloadHome
887
         * @function
888
         */
889
        me.reloadHome = function()
890
        {
891
            window.location.href = Helper.baseUri();
892
        }
893
894
        /**
895
         * checks whether the element is currently visible in the viewport (so
896
         * the user can actually see it)
897
         *
898
         * @see    {@link https://stackoverflow.com/a/40658647}
899
         * @name   UiHelper.isVisible
900
         * @function
901
         * @param  {jQuery} $element The link hash to move to.
902
         */
903
        me.isVisible = function($element)
904
        {
905
            var elementTop = $element.offset().top;
906
            var viewportTop = $(window).scrollTop();
907
            var viewportBottom = viewportTop + $(window).height();
908
909
            return (elementTop > viewportTop && elementTop < viewportBottom);
910
        }
911
912
        /**
913
         * scrolls to a specific element
914
         *
915
         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
916
         * @name   UiHelper.scrollTo
917
         * @function
918
         * @param  {jQuery}           $element        The link hash to move to.
919
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
920
         * @param  {string}           animationEffect   passed to jQuery .animate
921
         * @param  {function}         finishedCallback  function to call after animation finished
922
         */
923
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
924
        {
925
            var $body = $('html, body'),
926
                margin = 50,
927
                callbackCalled = false;
928
929
            //calculate destination place
930
            var dest = 0;
931
            // if it would scroll out of the screen at the bottom only scroll it as
932
            // far as the screen can go
933
            if ($element.offset().top > $(document).height() - $(window).height()) {
934
                dest = $(document).height() - $(window).height();
935
            } else {
936
                dest = $element.offset().top - margin;
937
            }
938
            // skip animation if duration is set to 0
939
            if (animationDuration === 0) {
940
                window.scrollTo(0, dest);
941
            } else {
942
                // stop previous animation
943
                $body.stop();
944
                // scroll to destination
945
                $body.animate({
946
                    scrollTop: dest
947
                }, animationDuration, animationEffect);
948
            }
949
950
            // as we have finished we can enable scrolling again
951
            $body.queue(function (next) {
952
                if (!callbackCalled) {
953
                    // call user function if needed
954
                    if (typeof finishedCallback !== 'undefined') {
955
                        finishedCallback();
956
                    }
957
958
                    // prevent calling this function twice
959
                    callbackCalled = true;
960
                }
961
                next();
962
            });
963
        }
964
965
        /**
966
         * initialize
967
         *
968
         * @name   UiHelper.init
969
         * @function
970
         */
971
        me.init = function()
972
        {
973
            // update link to home page
974
            $('.reloadlink').prop('href', Helper.baseUri());
975
976
            $(window).on('popstate', historyChange);
977
        }
978
979
        return me;
980
    })(window, document);
981
982
    /**
983
     * Alert/error manager
984
     *
985
     * @name   Alert
986
     * @class
987
     */
988
    var Alert = (function () {
989
        var me = {};
990
991
        var $errorMessage,
992
            $loadingIndicator,
993
            $statusMessage,
994
            $remainingTime;
995
996
        var currentIcon = [
997
            'glyphicon-time', // loading icon
998
            'glyphicon-info-sign', // status icon
999
            '', // resevered for warning, not used yet
1000
            'glyphicon-alert' // error icon
1001
        ];
1002
1003
        var alertType = [
1004
            'loading', // not in bootstrap, but using a good value here
1005
            'info', // status icon
1006
            'warning', // not used yet
1007
            'danger' // error icon
1008
        ];
1009
1010
        var customHandler;
1011
1012
        /**
1013
         * forwards a request to the i18n module and shows the element
1014
         *
1015
         * @name   Alert.handleNotification
1016
         * @private
1017
         * @function
1018
         * @param  {int} id - id of notification
1019
         * @param  {jQuery} $element - jQuery object
1020
         * @param  {string|array} args
1021
         * @param  {string|null} icon - optional, icon
1022
         */
1023
        function handleNotification(id, $element, args, icon)
1024
        {
1025
            // basic parsing/conversion of parameters
1026
            if (typeof icon === 'undefined') {
1027
                icon = null;
1028
            }
1029
            if (typeof args === 'undefined') {
1030
                args = null;
1031
            } else if (typeof args === 'string') {
1032
                // convert string to array if needed
1033
                args = [args];
1034
            }
1035
1036
            // pass to custom handler if defined
1037
            if (typeof customHandler === 'function') {
1038
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1039
                if (handlerResult === true) {
1040
                    // if it returs true, skip own handler
1041
                    return;
1042
                }
1043
                if (handlerResult instanceof jQuery) {
1044
                    // continue processing with new element
1045
                    $element = handlerResult;
1046
                    icon = null; // icons not supported in this case
1047
                }
1048
            }
1049
1050
            // handle icon
1051
            if (icon !== null && // icon was passed
1052
                icon !== currentIcon[id] // and it differs from current icon
1053
            ) {
1054
                var $glyphIcon = $element.find(':first');
1055
1056
                // remove (previous) icon
1057
                $glyphIcon.removeClass(currentIcon[id]);
1058
1059
                // any other thing as a string (e.g. 'null') (only) removes the icon
1060
                if (typeof icon === 'string') {
1061
                    // set new icon
1062
                    currentIcon[id] = 'glyphicon-' + icon;
1063
                    $glyphIcon.addClass(currentIcon[id]);
1064
                }
1065
            }
1066
1067
            // show text
1068
            if (args !== null) {
1069
                // add jQuery object to it as first parameter
1070
                args.unshift($element);
1071
                // pass it to I18n
1072
                I18n._.apply(this, args);
1073
            }
1074
1075
            // show notification
1076
            $element.removeClass('hidden');
1077
        }
1078
1079
        /**
1080
         * display a status message
1081
         *
1082
         * This automatically passes the text to I18n for translation.
1083
         *
1084
         * @name   Alert.showStatus
1085
         * @function
1086
         * @param  {string|array} message     string, use an array for %s/%d options
1087
         * @param  {string|null}  icon        optional, the icon to show,
1088
         *                                    default: leave previous icon
1089
         * @param  {bool}         dismissable optional, whether the notification
1090
         *                                    can be dismissed (closed), default: false
1091
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1092
         *                                    notification should be hidden automatically;
1093
         *                                    default: disabled (0); use true for default value
1094
         */
1095
        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...
1096
        {
1097
            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...
1098
            // @TODO: implement dismissable
1099
            // @TODO: implement autoclose
1100
1101
            handleNotification(1, $statusMessage, message, icon);
1102
        }
1103
1104
        /**
1105
         * display an error message
1106
         *
1107
         * This automatically passes the text to I18n for translation.
1108
         *
1109
         * @name   Alert.showError
1110
         * @function
1111
         * @param  {string|array} message     string, use an array for %s/%d options
1112
         * @param  {string|null}  icon        optional, the icon to show, default:
1113
         *                                    leave previous icon
1114
         * @param  {bool}         dismissable optional, whether the notification
1115
         *                                    can be dismissed (closed), default: false
1116
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1117
         *                                    notification should be hidden automatically;
1118
         *                                    default: disabled (0); use true for default value
1119
         */
1120
        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...
1121
        {
1122
            console.error('error message shown: ', message);
1123
            // @TODO: implement dismissable (bootstrap add-on has it)
1124
            // @TODO: implement autoclose
1125
1126
            handleNotification(3, $errorMessage, message, icon);
1127
        }
1128
1129
        /**
1130
         * display remaining message
1131
         *
1132
         * This automatically passes the text to I18n for translation.
1133
         *
1134
         * @name   Alert.showRemaining
1135
         * @function
1136
         * @param  {string|array} message     string, use an array for %s/%d options
1137
         */
1138
        me.showRemaining = function(message)
1139
        {
1140
            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...
1141
            handleNotification(1, $remainingTime, message);
1142
        }
1143
1144
        /**
1145
         * shows a loading message, optionally with a percentage
1146
         *
1147
         * This automatically passes all texts to the i10s module.
1148
         *
1149
         * @name   Alert.showLoading
1150
         * @function
1151
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1152
         * @param  {int}               percentage   optional, default: null
1153
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1154
         */
1155
        me.showLoading = function(message, percentage, icon)
1156
        {
1157
            if (typeof message !== 'undefined' && message !== null) {
1158
                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...
1159
            }
1160
1161
            // default message text
1162
            if (typeof message === 'undefined') {
1163
                message = 'Loading…';
1164
            }
1165
1166
            // currently percentage parameter is ignored
1167
            // // @TODO handle it here…
1168
1169
            handleNotification(0, $loadingIndicator, message, icon);
1170
1171
            // show loading status (cursor)
1172
            $('body').addClass('loading');
1173
        }
1174
1175
        /**
1176
         * hides the loading message
1177
         *
1178
         * @name   Alert.hideLoading
1179
         * @function
1180
         */
1181
        me.hideLoading = function()
1182
        {
1183
            $loadingIndicator.addClass('hidden');
1184
1185
            // hide loading cursor
1186
            $('body').removeClass('loading');
1187
        }
1188
1189
        /**
1190
         * hides any status/error messages
1191
         *
1192
         * This does not include the loading message.
1193
         *
1194
         * @name   Alert.hideMessages
1195
         * @function
1196
         */
1197
        me.hideMessages = function()
1198
        {
1199
            // also possible: $('.statusmessage').addClass('hidden');
1200
            $statusMessage.addClass('hidden');
1201
            $errorMessage.addClass('hidden');
1202
        }
1203
1204
        /**
1205
         * set a custom handler, which gets all notifications.
1206
         *
1207
         * This handler gets the following arguments:
1208
         * alertType (see array), $element, args, icon
1209
         * If it returns true, the own processing will be stopped so the message
1210
         * will not be displayed. Otherwise it will continue.
1211
         * As an aditional feature it can return q jQuery element, which will
1212
         * then be used to add the message there. Icons are not supported in
1213
         * that case and will be ignored.
1214
         * Pass 'null' to reset/delete the custom handler.
1215
         * Note that there is no notification when a message is supposed to get
1216
         * hidden.
1217
         *
1218
         * @name   Alert.setCustomHandler
1219
         * @function
1220
         * @param {function|null} newHandler
1221
         */
1222
        me.setCustomHandler = function(newHandler)
1223
        {
1224
            customHandler = newHandler;
1225
        }
1226
1227
        /**
1228
         * init status manager
1229
         *
1230
         * preloads jQuery elements
1231
         *
1232
         * @name   Alert.init
1233
         * @function
1234
         */
1235
        me.init = function()
1236
        {
1237
            // hide "no javascript" error message
1238
            $('#noscript').hide();
1239
1240
            // not a reset, but first set of the elements
1241
            $errorMessage = $('#errormessage');
1242
            $loadingIndicator = $('#loadingindicator');
1243
            $statusMessage = $('#status');
1244
            $remainingTime = $('#remainingtime');
1245
        }
1246
1247
        return me;
1248
    })();
1249
1250
    /**
1251
     * handles paste status/result
1252
     *
1253
     * @name   PasteStatus
1254
     * @param  {object} window
1255
     * @class
1256
     */
1257
    var PasteStatus = (function (window) {
1258
        var me = {};
1259
1260
        var $pasteSuccess,
1261
            $pasteUrl,
1262
            $remainingTime,
1263
            $shortenButton;
1264
1265
        /**
1266
         * forward to URL shortener
1267
         *
1268
         * @name   PasteStatus.sendToShortener
1269
         * @private
1270
         * @function
1271
         * @param  {Event} event
1272
         */
1273
        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...
1274
        {
1275
            window.location.href = $shortenButton.data('shortener')
1276
                                   + encodeURIComponent($pasteUrl.attr('href'));
1277
        }
1278
1279
        /**
1280
         * Forces opening the paste if the link does not do this automatically.
1281
         *
1282
         * This is necessary as browsers will not reload the page when it is
1283
         * already loaded (which is fake as it is set via history.pushState()).
1284
         *
1285
         * @name   PasteStatus.pasteLinkClick
1286
         * @function
1287
         * @param  {Event} event
1288
         */
1289
        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...
1290
        {
1291
            // check if location is (already) shown in URL bar
1292
            if (window.location.href === $pasteUrl.attr('href')) {
1293
                // if so we need to load link by reloading the current site
1294
                window.location.reload(true);
1295
            }
1296
        }
1297
1298
        /**
1299
         * creates a notification after a successfull paste upload
1300
         *
1301
         * @name   PasteStatus.createPasteNotification
1302
         * @function
1303
         * @param  {string} url
1304
         * @param  {string} deleteUrl
1305
         */
1306
        me.createPasteNotification = function(url, deleteUrl)
1307
        {
1308
            $('#pastelink').html(
1309
                I18n._(
1310
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1311
                    url, url
1312
                )
1313
            );
1314
            // save newly created element
1315
            $pasteUrl = $('#pasteurl');
1316
            // and add click event
1317
            $pasteUrl.click(pasteLinkClick);
1318
1319
            // shorten button
1320
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1321
1322
            // show result
1323
            $pasteSuccess.removeClass('hidden');
1324
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1325
            Helper.selectText($pasteUrl[0]);
1326
        }
1327
1328
        /**
1329
         * shows the remaining time
1330
         *
1331
         * @name PasteStatus.showRemainingTime
1332
         * @function
1333
         * @param {object} pasteMetaData
1334
         */
1335
        me.showRemainingTime = function(pasteMetaData)
1336
        {
1337
            if (pasteMetaData.burnafterreading) {
1338
                // display paste "for your eyes only" if it is deleted
1339
1340
                // actually remove paste, before we claim it is deleted
1341
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1342
1343
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1344
                $remainingTime.addClass('foryoureyesonly');
1345
1346
                // discourage cloning (it cannot really be prevented)
1347
                TopNav.hideCloneButton();
1348
1349
            } else if (pasteMetaData.expire_date) {
1350
                // display paste expiration
1351
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1352
                    expirationLabel = [
1353
                        'This document will expire in %d ' + expiration[1] + '.',
1354
                        'This document will expire in %d ' + expiration[1] + 's.'
1355
                    ];
1356
1357
                Alert.showRemaining([expirationLabel, expiration[0]]);
1358
                $remainingTime.removeClass('foryoureyesonly')
1359
            } else {
1360
                // never expires
1361
                return;
1362
            }
1363
1364
            // in the end, display notification
1365
            $remainingTime.removeClass('hidden');
1366
        }
1367
1368
        /**
1369
         * hides the remaining time and successful upload notification
1370
         *
1371
         * @name PasteStatus.hideRemainingTime
1372
         * @function
1373
         */
1374
        me.hideMessages = function()
1375
        {
1376
            $remainingTime.addClass('hidden');
1377
            $pasteSuccess.addClass('hidden');
1378
        }
1379
1380
        /**
1381
         * init status manager
1382
         *
1383
         * preloads jQuery elements
1384
         *
1385
         * @name   PasteStatus.init
1386
         * @function
1387
         */
1388
        me.init = function()
1389
        {
1390
            $pasteSuccess = $('#pasteSuccess');
1391
            // $pasteUrl is saved in me.createPasteNotification() after creation
1392
            $remainingTime = $('#remainingtime');
1393
            $shortenButton = $('#shortenbutton');
1394
1395
            // bind elements
1396
            $shortenButton.click(sendToShortener);
1397
        }
1398
1399
        return me;
1400
    })(window);
1401
1402
    /**
1403
     * password prompt
1404
     *
1405
     * @name Prompt
1406
     * @class
1407
     */
1408
    var Prompt = (function () {
1409
        var me = {};
1410
1411
        var $passwordDecrypt,
1412
            $passwordForm,
1413
            $passwordModal;
1414
1415
        var password = '';
1416
1417
        /**
1418
         * submit a password in the modal dialog
1419
         *
1420
         * @name Prompt.submitPasswordModal
1421
         * @private
1422
         * @function
1423
         * @param  {Event} event
1424
         */
1425
        function submitPasswordModal(event)
1426
        {
1427
            event.preventDefault();
1428
1429
            // get input
1430
            password = $passwordDecrypt.val();
1431
1432
            // hide modal
1433
            $passwordModal.modal('hide');
1434
1435
            PasteDecrypter.run();
1436
        }
1437
1438
        /**
1439
         * ask the user for the password and set it
1440
         *
1441
         * @name Prompt.requestPassword
1442
         * @function
1443
         */
1444
        me.requestPassword = function()
1445
        {
1446
            // show new bootstrap method (if available)
1447
            if ($passwordModal.length !== 0) {
1448
                $passwordModal.modal({
1449
                    backdrop: 'static',
1450
                    keyboard: false
1451
                });
1452
                return;
1453
            }
1454
1455
            // fallback to old method for page template
1456
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1457
            if (newPassword === null) {
1458
                throw 'password prompt canceled';
1459
            }
1460
            if (password.length === 0) {
1461
                // recursive…
1462
                return me.requestPassword();
1463
            }
1464
1465
            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...
1466
        }
1467
1468
        /**
1469
         * getthe cached password
1470
         *
1471
         * If you do not get a password with this function
1472
         * (returns an empty string), use requestPassword.
1473
         *
1474
         * @name   Prompt.getPassword
1475
         * @function
1476
         * @return {string}
1477
         */
1478
        me.getPassword = function()
1479
        {
1480
            return password;
1481
        }
1482
1483
        /**
1484
         * init status manager
1485
         *
1486
         * preloads jQuery elements
1487
         *
1488
         * @name   Prompt.init
1489
         * @function
1490
         */
1491
        me.init = function()
1492
        {
1493
            $passwordDecrypt = $('#passworddecrypt');
1494
            $passwordForm = $('#passwordform');
1495
            $passwordModal = $('#passwordmodal');
1496
1497
            // bind events
1498
1499
            // focus password input when it is shown
1500
            $passwordModal.on('shown.bs.Model', function () {
1501
                $passwordDecrypt.focus();
1502
            });
1503
            // handle Model password submission
1504
            $passwordForm.submit(submitPasswordModal);
1505
        }
1506
1507
        return me;
1508
    })();
1509
1510
    /**
1511
     * Manage paste/message input, and preview tab
1512
     *
1513
     * Note that the actual preview is handled by PasteViewer.
1514
     *
1515
     * @name   Editor
1516
     * @class
1517
     */
1518
    var Editor = (function () {
1519
        var me = {};
1520
1521
        var $editorTabs,
1522
            $messageEdit,
1523
            $messagePreview,
1524
            $message;
1525
1526
        var isPreview = false;
1527
1528
        /**
1529
         * support input of tab character
1530
         *
1531
         * @name   Editor.supportTabs
1532
         * @function
1533
         * @param  {Event} event
1534
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1535
         */
1536
        function supportTabs(event)
1537
        {
1538
            var keyCode = event.keyCode || event.which;
1539
            // tab was pressed
1540
            if (keyCode === 9) {
1541
                // get caret position & selection
1542
                var val   = this.value,
1543
                    start = this.selectionStart,
1544
                    end   = this.selectionEnd;
1545
                // set textarea value to: text before caret + tab + text after caret
1546
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1547
                // put caret at right position again
1548
                this.selectionStart = this.selectionEnd = start + 1;
1549
                // prevent the textarea to lose focus
1550
                event.preventDefault();
1551
            }
1552
        }
1553
1554
        /**
1555
         * view the Editor tab
1556
         *
1557
         * @name   Editor.viewEditor
1558
         * @function
1559
         * @param  {Event} event - optional
1560
         */
1561
        function viewEditor(event)
1562
        {
1563
            // toggle buttons
1564
            $messageEdit.addClass('active');
1565
            $messagePreview.removeClass('active');
1566
1567
            PasteViewer.hide();
1568
1569
            // reshow input
1570
            $message.removeClass('hidden');
1571
1572
            me.focusInput();
1573
1574
            // finish
1575
            isPreview = false;
1576
1577
            // prevent jumping of page to top
1578
            if (typeof event !== 'undefined') {
1579
                event.preventDefault();
1580
            }
1581
        }
1582
1583
        /**
1584
         * view the preview tab
1585
         *
1586
         * @name   Editor.viewPreview
1587
         * @function
1588
         * @param  {Event} event
1589
         */
1590
        function viewPreview(event)
1591
        {
1592
            // toggle buttons
1593
            $messageEdit.removeClass('active');
1594
            $messagePreview.addClass('active');
1595
1596
            // hide input as now preview is shown
1597
            $message.addClass('hidden');
1598
1599
            // show preview
1600
            PasteViewer.setText($message.val());
1601
            PasteViewer.run();
1602
1603
            // finish
1604
            isPreview = true;
1605
1606
            // prevent jumping of page to top
1607
            if (typeof event !== 'undefined') {
1608
                event.preventDefault();
1609
            }
1610
        }
1611
1612
        /**
1613
         * get the state of the preview
1614
         *
1615
         * @name   Editor.isPreview
1616
         * @function
1617
         */
1618
        me.isPreview = function()
1619
        {
1620
            return isPreview;
1621
        }
1622
1623
        /**
1624
         * reset the Editor view
1625
         *
1626
         * @name   Editor.resetInput
1627
         * @function
1628
         */
1629
        me.resetInput = function()
1630
        {
1631
            // go back to input
1632
            if (isPreview) {
1633
                viewEditor();
1634
            }
1635
1636
            // clear content
1637
            $message.val('');
1638
        }
1639
1640
        /**
1641
         * shows the Editor
1642
         *
1643
         * @name   Editor.show
1644
         * @function
1645
         */
1646
        me.show = function()
1647
        {
1648
            $message.removeClass('hidden');
1649
            $editorTabs.removeClass('hidden');
1650
        }
1651
1652
        /**
1653
         * hides the Editor
1654
         *
1655
         * @name   Editor.reset
1656
         * @function
1657
         */
1658
        me.hide = function()
1659
        {
1660
            $message.addClass('hidden');
1661
            $editorTabs.addClass('hidden');
1662
        }
1663
1664
        /**
1665
         * focuses the message input
1666
         *
1667
         * @name   Editor.focusInput
1668
         * @function
1669
         */
1670
        me.focusInput = function()
1671
        {
1672
            $message.focus();
1673
        }
1674
1675
        /**
1676
         * sets a new text
1677
         *
1678
         * @name   Editor.setText
1679
         * @function
1680
         * @param {string} newText
1681
         */
1682
        me.setText = function(newText)
1683
        {
1684
            $message.val(newText);
1685
        }
1686
1687
        /**
1688
         * returns the current text
1689
         *
1690
         * @name   Editor.getText
1691
         * @function
1692
         * @return {string}
1693
         */
1694
        me.getText = function()
1695
        {
1696
            return $message.val()
1697
        }
1698
1699
        /**
1700
         * init status manager
1701
         *
1702
         * preloads jQuery elements
1703
         *
1704
         * @name   Editor.init
1705
         * @function
1706
         */
1707
        me.init = function()
1708
        {
1709
            $editorTabs = $('#editorTabs');
1710
            $message = $('#message');
1711
1712
            // bind events
1713
            $message.keydown(supportTabs);
1714
1715
            // bind click events to tab switchers (a), but save parent of them
1716
            // (li)
1717
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1718
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1719
        }
1720
1721
        return me;
1722
    })();
1723
1724
    /**
1725
     * (view) Parse and show paste.
1726
     *
1727
     * @name   PasteViewer
1728
     * @class
1729
     */
1730
    var PasteViewer = (function () {
1731
        var me = {};
1732
1733
        var $placeholder,
1734
            $prettyMessage,
1735
            $prettyPrint,
1736
            $plainText;
1737
1738
        var text,
1739
            format = 'plaintext',
1740
            isDisplayed = false,
1741
            isChanged = true; // by default true as nothing was parsed yet
1742
1743
        /**
1744
         * apply the set format on paste and displays it
1745
         *
1746
         * @name   PasteViewer.parsePaste
1747
         * @private
1748
         * @function
1749
         */
1750
        function parsePaste()
1751
        {
1752
            // skip parsing if no text is given
1753
            if (text === '') {
1754
                return;
1755
            }
1756
1757
            // set text
1758
            Helper.setElementText($plainText, text);
1759
            Helper.setElementText($prettyPrint, text);
1760
1761
            switch (format) {
1762
                case 'markdown':
1763
                    var converter = new showdown.Converter({
1764
                        strikethrough: true,
1765
                        tables: true,
1766
                        tablesHeaderId: true
1767
                    });
1768
                    $plainText.html(
1769
                        converter.makeHtml(text)
1770
                    );
1771
                    // add table classes from bootstrap css
1772
                    $plainText.find('table').addClass('table-condensed table-bordered');
1773
                    break;
1774
                case 'syntaxhighlighting':
1775
                    // @TODO is this really needed or is "one" enough?
1776
                    if (typeof prettyPrint === 'function')
1777
                    {
1778
                        prettyPrint();
1779
                    }
1780
1781
                    $prettyPrint.html(
1782
                        prettyPrintOne(
1783
                            Helper.htmlEntities(text), null, true
1784
                        )
1785
                    );
1786
                    // fall through, as the rest is the same
1787
                default: // = 'plaintext'
1788
                    // convert URLs to clickable links
1789
                    Helper.urls2links($plainText);
1790
                    Helper.urls2links($prettyPrint);
1791
1792
                    $prettyPrint.css('white-space', 'pre-wrap');
1793
                    $prettyPrint.css('word-break', 'normal');
1794
                    $prettyPrint.removeClass('prettyprint');
1795
            }
1796
        }
1797
1798
        /**
1799
         * displays the paste
1800
         *
1801
         * @name   PasteViewer.showPaste
1802
         * @private
1803
         * @function
1804
         */
1805
        function showPaste()
1806
        {
1807
            // instead of "nothing" better display a placeholder
1808
            if (text === '') {
1809
                $placeholder.removeClass('hidden')
1810
                return;
1811
            }
1812
            // otherwise hide the placeholder
1813
            $placeholder.addClass('hidden')
1814
1815
            switch (format) {
1816
                case 'markdown':
1817
                    $plainText.removeClass('hidden');
1818
                    $prettyMessage.addClass('hidden');
1819
                    break;
1820
                default:
1821
                    $plainText.addClass('hidden');
1822
                    $prettyMessage.removeClass('hidden');
1823
                    break;
1824
            }
1825
        }
1826
1827
        /**
1828
         * sets the format in which the text is shown
1829
         *
1830
         * @name   PasteViewer.setFormat
1831
         * @function
1832
         * @param {string} newFormat the the new format
1833
         */
1834
        me.setFormat = function(newFormat)
1835
        {
1836
            // skip if there is no update
1837
            if (format === newFormat) {
1838
                return;
1839
            }
1840
1841
            // needs to update display too, if from or to Markdown is switched
1842
            if (format === 'markdown' || newFormat === 'markdown') {
1843
                isDisplayed = false;
1844
            }
1845
1846
            format = newFormat;
1847
            isChanged = true;
1848
        }
1849
1850
        /**
1851
         * returns the current format
1852
         *
1853
         * @name   PasteViewer.getFormat
1854
         * @function
1855
         * @return {string}
1856
         */
1857
        me.getFormat = function()
1858
        {
1859
            return format;
1860
        }
1861
1862
        /**
1863
         * returns whether the current view is pretty printed
1864
         *
1865
         * @name   PasteViewer.isPrettyPrinted
1866
         * @function
1867
         * @return {bool}
1868
         */
1869
        me.isPrettyPrinted = function()
1870
        {
1871
            return $prettyPrint.hasClass('prettyprinted');
1872
        }
1873
1874
        /**
1875
         * sets the text to show
1876
         *
1877
         * @name   PasteViewer.setText
1878
         * @function
1879
         * @param {string} newText the text to show
1880
         */
1881
        me.setText = function(newText)
1882
        {
1883
            if (text !== newText) {
1884
                text = newText;
1885
                isChanged = true;
1886
            }
1887
        }
1888
1889
        /**
1890
         * gets the current cached text
1891
         *
1892
         * @name   PasteViewer.getText
1893
         * @function
1894
         * @return {string}
1895
         */
1896
        me.getText = function()
1897
        {
1898
            return text;
1899
        }
1900
1901
        /**
1902
         * show/update the parsed text (preview)
1903
         *
1904
         * @name   PasteViewer.run
1905
         * @function
1906
         */
1907
        me.run = function()
1908
        {
1909
            if (isChanged) {
1910
                parsePaste();
1911
                isChanged = false;
1912
            }
1913
1914
            if (!isDisplayed) {
1915
                showPaste();
1916
                isDisplayed = true;
1917
            }
1918
        }
1919
1920
        /**
1921
         * hide parsed text (preview)
1922
         *
1923
         * @name   PasteViewer.hide
1924
         * @function
1925
         */
1926
        me.hide = function()
1927
        {
1928
            if (!isDisplayed) {
1929
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1930
            }
1931
1932
            $plainText.addClass('hidden');
1933
            $prettyMessage.addClass('hidden');
1934
            $placeholder.addClass('hidden');
1935
1936
            isDisplayed = false;
1937
        }
1938
1939
        /**
1940
         * init status manager
1941
         *
1942
         * preloads jQuery elements
1943
         *
1944
         * @name   PasteViewer.init
1945
         * @function
1946
         */
1947
        me.init = function()
1948
        {
1949
            $placeholder = $('#placeholder');
1950
            $plainText = $('#plaintext');
1951
            $prettyMessage = $('#prettymessage');
1952
            $prettyPrint = $('#prettyprint');
1953
1954
            // check requirements
1955
            if (typeof prettyPrintOne !== 'function') {
1956
                Alert.showError([
1957
                    'The library %s is not available. This may cause display errors.',
1958
                    'pretty print'
1959
                ]);
1960
            }
1961
            if (typeof showdown !== 'object') {
1962
                Alert.showError([
1963
                    'The library %s is not available. This may cause display errors.',
1964
                    'showdown'
1965
                ]);
1966
            }
1967
1968
            // get default option from template/HTML or fall back to set value
1969
            format = Model.getFormatDefault() || format;
1970
        }
1971
1972
        return me;
1973
    })();
1974
1975
    /**
1976
     * (view) Show attachment and preview if possible
1977
     *
1978
     * @name   AttachmentViewer
1979
     * @param  {object} window
1980
     * @param  {object} document
1981
     * @class
1982
     */
1983
    var AttachmentViewer = (function (window, document) {
1984
        var me = {};
1985
1986
        var $attachmentLink,
1987
            $attachmentPreview,
1988
            $attachment;
1989
1990
        var attachmentHasPreview = false;
1991
1992
        /**
1993
         * sets the attachment but does not yet show it
1994
         *
1995
         * @name   AttachmentViewer.setAttachment
1996
         * @function
1997
         * @param {string} attachmentData - base64-encoded data of file
1998
         * @param {string} fileName - optional, file name
1999
         */
2000
        me.setAttachment = function(attachmentData, fileName)
2001
        {
2002
            var imagePrefix = 'data:image/';
2003
2004
            $attachmentLink.attr('href', attachmentData);
2005
            if (typeof fileName !== 'undefined') {
2006
                $attachmentLink.attr('download', fileName);
2007
            }
2008
2009
            // if the attachment is an image, display it
2010
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2011
                $attachmentPreview.html(
2012
                    $(document.createElement('img'))
2013
                        .attr('src', attachmentData)
2014
                        .attr('class', 'img-thumbnail')
2015
                );
2016
                attachmentHasPreview = true;
2017
            }
2018
        }
2019
2020
        /**
2021
         * displays the attachment
2022
         *
2023
         * @name AttachmentViewer.showAttachment
2024
         * @function
2025
         */
2026
        me.showAttachment = function()
2027
        {
2028
            $attachment.removeClass('hidden');
2029
2030
            if (attachmentHasPreview) {
2031
                $attachmentPreview.removeClass('hidden');
2032
            }
2033
        }
2034
2035
        /**
2036
         * removes the attachment
2037
         *
2038
         * This automatically hides the attachment containers to, to
2039
         * prevent an inconsistent display.
2040
         *
2041
         * @name AttachmentViewer.removeAttachment
2042
         * @function
2043
         */
2044
        me.removeAttachment = function()
2045
        {
2046
            me.hideAttachment();
2047
            me.hideAttachmentPreview();
2048
            $attachmentLink.prop('href', '');
2049
            $attachmentLink.prop('download', '');
2050
            $attachmentPreview.html('');
2051
        }
2052
2053
        /**
2054
         * hides the attachment
2055
         *
2056
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2057
         * for that) nor will it hide the attachment link if it was moved somewhere
2058
         * else (see AttachmentViewer.moveAttachmentTo).
2059
         *
2060
         * @name AttachmentViewer.hideAttachment
2061
         * @function
2062
         */
2063
        me.hideAttachment = function()
2064
        {
2065
            $attachment.addClass('hidden');
2066
        }
2067
2068
        /**
2069
         * hides the attachment preview
2070
         *
2071
         * @name AttachmentViewer.hideAttachmentPreview
2072
         * @function
2073
         */
2074
        me.hideAttachmentPreview = function()
2075
        {
2076
            $attachmentPreview.addClass('hidden');
2077
        }
2078
2079
        /**
2080
         * checks if there is an attachment
2081
         *
2082
         * @name   AttachmentViewer.hasAttachment
2083
         * @function
2084
         */
2085
        me.hasAttachment = function()
2086
        {
2087
            return ($attachmentLink.prop('href') !== '')
2088
        }
2089
2090
        /**
2091
         * return the attachment
2092
         *
2093
         * @name   AttachmentViewer.getAttachment
2094
         * @function
2095
         * @returns {array}
2096
         */
2097
        me.getAttachment = function()
2098
        {
2099
            return [
2100
                $attachmentLink.prop('href'),
2101
                $attachmentLink.prop('download')
2102
            ];
2103
        }
2104
2105
        /**
2106
         * moves the attachment link to another element
2107
         *
2108
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2109
         *
2110
         * @name   AttachmentViewer.moveAttachmentTo
2111
         * @function
2112
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2113
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2114
         */
2115
        me.moveAttachmentTo = function($element, label)
2116
        {
2117
            // move elemement to new place
2118
            $attachmentLink.appendTo($element);
2119
2120
            // update text
2121
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2122
        }
2123
2124
        /**
2125
         * initiate
2126
         *
2127
         * preloads jQuery elements
2128
         *
2129
         * @name   AttachmentViewer.init
2130
         * @function
2131
         */
2132
        me.init = function()
2133
        {
2134
            $attachment = $('#attachment');
2135
            $attachmentLink = $('#attachment a');
2136
            $attachmentPreview = $('#attachmentPreview');
2137
        }
2138
2139
        return me;
2140
    })(window, document);
2141
2142
    /**
2143
     * (view) Shows discussion thread and handles replies
2144
     *
2145
     * @name   DiscussionViewer
2146
     * @param  {object} window
2147
     * @param  {object} document
2148
     * @class
2149
     */
2150
    var DiscussionViewer = (function (window, document) {
2151
        var me = {};
2152
2153
        var $commentTail,
2154
            $discussion,
2155
            $reply,
2156
            $replyMessage,
2157
            $replyNickname,
2158
            $replyStatus,
2159
            $commentContainer;
2160
2161
        var replyCommentId;
2162
2163
        /**
2164
         * initializes the templates
2165
         *
2166
         * @name   DiscussionViewer.initTemplates
2167
         * @private
2168
         * @function
2169
         */
2170
        function initTemplates()
2171
        {
2172
            $reply = Model.getTemplate('reply');
2173
            $replyMessage = $reply.find('#replymessage');
2174
            $replyNickname = $reply.find('#nickname');
2175
            $replyStatus = $reply.find('#replystatus');
2176
2177
            // cache jQuery elements
2178
            $commentTail = Model.getTemplate('commenttail');
2179
        }
2180
2181
        /**
2182
         * open the comment entry when clicking the "Reply" button of a comment
2183
         *
2184
         * @name   DiscussionViewer.openReply
2185
         * @private
2186
         * @function
2187
         * @param  {Event} event
2188
         */
2189
        function openReply(event)
2190
        {
2191
            var $source = $(event.target);
2192
2193
            // clear input
2194
            $replyMessage.val('');
2195
            $replyNickname.val('');
2196
2197
            // get comment id from source element
2198
            replyCommentId = $source.parent().prop('id').split('_')[1];
2199
2200
            // move to correct position
2201
            $source.after($reply);
2202
2203
            // show
2204
            $reply.removeClass('hidden');
2205
            $replyMessage.focus();
2206
2207
            event.preventDefault();
2208
        }
2209
2210
        /**
2211
         * custom handler for displaying notifications in own status message area
2212
         *
2213
         * @name   DiscussionViewer.handleNotification
2214
         * @function
2215
         * @param  {string} alertType
2216
         * @param  {jQuery} $element
2217
         * @param  {string|array} args
2218
         * @param  {string|null} icon
2219
         * @return {bool|jQuery}
2220
         */
2221
        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 $element is not used and could be removed.

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

Loading history...
The parameter 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...
2222
        {
2223
            // ignore loading messages
2224
            if (alertType === 'loading') {
2225
                return false;
2226
            }
2227
2228
            if (alertType === 'danger') {
2229
                $replyStatus.removeClass('alert-info');
2230
                $replyStatus.addClass('alert-danger');
2231
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2232
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2233
            } else {
2234
                $replyStatus.removeClass('alert-danger');
2235
                $replyStatus.addClass('alert-info');
2236
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2237
                $replyStatus.find(':first').addClass('glyphicon-alert');
2238
            }
2239
2240
            return $replyStatus;
2241
        }
2242
2243
        /**
2244
         * adds another comment
2245
         *
2246
         * @name   DiscussionViewer.addComment
2247
         * @function
2248
         * @param {object} comment
2249
         * @param {string} commentText
2250
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2251
         */
2252
        me.addComment = function(comment, commentText, nickname, $place)
2253
        {
2254
            if (typeof $place === 'undefined') {
2255
                // starting point (default value/fallback)
2256
                $place = $commentContainer;
2257
2258
                // if parent comment exists
2259
                var $parentComment = $('#comment_' + comment.parentid);
2260
                if ($parentComment.length) {
2261
                    // use parent as position for noew comment, so it shifted
2262
                    // to the right
2263
                    $place = $parentComment;
2264
                }
2265
            }
2266
            if (commentText === '') {
2267
                commentText = 'comment decryption failed';
2268
            }
2269
2270
            // create new comment based on template
2271
            var $commentEntry = Model.getTemplate('comment');
2272
            $commentEntry.prop('id', 'comment_' + comment.id);
2273
            var $commentEntryData = $commentEntry.find('div.commentdata');
2274
2275
            // set & parse text
2276
            Helper.setElementText($commentEntryData, commentText);
2277
            Helper.urls2links($commentEntryData);
2278
2279
            // set nickname
2280
            if (nickname.length > 0) {
2281
                $commentEntry.find('span.nickname').text(nickname);
2282
            } else {
2283
                $commentEntry.find('span.nickname').html('<i></i>');
2284
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2285
            }
2286
2287
            // set date
2288
            $commentEntry.find('span.commentdate')
2289
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2290
                      .attr('title', 'CommentID: ' + comment.id);
2291
2292
            // if an avatar is available, display it
2293
            if (comment.meta.vizhash) {
2294
                $commentEntry.find('span.nickname')
2295
                             .before(
2296
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2297
                             );
2298
                $(document).on('languageLoaded', function () {
2299
                    $commentEntry.find('img.vizhash')
2300
                                 .prop('title', I18n._('Avatar generated from IP address'));
2301
                });
2302
            }
2303
2304
            // finally append comment
2305
            $place.append($commentEntry);
2306
        }
2307
2308
        /**
2309
         * finishes the discussion area after last comment
2310
         *
2311
         * @name   DiscussionViewer.finishDiscussion
2312
         * @function
2313
         */
2314
        me.finishDiscussion = function()
2315
        {
2316
            // add 'add new comment' area
2317
            $commentContainer.append($commentTail);
2318
2319
            // show discussions
2320
            $discussion.removeClass('hidden');
2321
        }
2322
2323
        /**
2324
         * shows the discussion area
2325
         *
2326
         * @name   DiscussionViewer.showDiscussion
2327
         * @function
2328
         */
2329
        me.showDiscussion = function()
2330
        {
2331
            $discussion.removeClass('hidden');
2332
        }
2333
2334
        /**
2335
         * removes the old discussion and prepares everything for creating a new
2336
         * one.
2337
         *
2338
         * @name   DiscussionViewer.prepareNewDisucssion
2339
         * @function
2340
         */
2341
        me.prepareNewDisucssion = function()
2342
        {
2343
            $commentContainer.html('');
2344
            $discussion.addClass('hidden');
2345
2346
            // (re-)init templates
2347
            initTemplates();
2348
        }
2349
2350
        /**
2351
         * returns the user put into the reply form
2352
         *
2353
         * @name   DiscussionViewer.getReplyData
2354
         * @function
2355
         * @return {array}
2356
         */
2357
        me.getReplyData = function()
2358
        {
2359
            return [
2360
                $replyMessage.val(),
2361
                $replyNickname.val()
2362
            ];
2363
        }
2364
2365
        /**
2366
         * highlights a specific comment and scrolls to it if necessary
2367
         *
2368
         * @name   DiscussionViewer.highlightComment
2369
         * @function
2370
         * @param {string} commentId
2371
         * @param {bool} fadeOut - whether to fade out the comment
2372
         */
2373
        me.highlightComment = function(commentId, fadeOut)
2374
        {
2375
            var $comment = $('#comment_' + commentId);
2376
            // in case comment does not exist, cancel
2377
            if ($comment.length === 0) {
2378
                return;
2379
            }
2380
2381
            var highlightComment = function () {
2382
                $comment.addClass('highlight');
2383
                if (fadeOut === true) {
2384
                    setTimeout(function () {
2385
                        $comment.removeClass('highlight');
2386
                    }, 300);
2387
                }
2388
            }
2389
2390
            if (UiHelper.isVisible($comment)) {
2391
                return highlightComment();
2392
            }
2393
2394
            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...
2395
        }
2396
2397
        /**
2398
         * returns the id of the parent comment the user is replying to
2399
         *
2400
         * @name   DiscussionViewer.getReplyCommentId
2401
         * @function
2402
         * @return {int|undefined}
2403
         */
2404
        me.getReplyCommentId = function()
2405
        {
2406
            return replyCommentId;
2407
        }
2408
2409
        /**
2410
         * initiate
2411
         *
2412
         * preloads jQuery elements
2413
         *
2414
         * @name   DiscussionViewer.init
2415
         * @function
2416
         */
2417
        me.init = function()
2418
        {
2419
            // bind events to templates (so they are later cloned)
2420
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2421
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2422
2423
            $commentContainer = $('#commentcontainer');
2424
            $discussion = $('#discussion');
2425
        }
2426
2427
        return me;
2428
    })(window, document);
2429
2430
    /**
2431
     * Manage top (navigation) bar
2432
     *
2433
     * @name   TopNav
2434
     * @param  {object} window
2435
     * @param  {object} document
2436
     * @class
2437
     */
2438
    var TopNav = (function (window, document) {
2439
        var me = {};
2440
2441
        var createButtonsDisplayed = false;
2442
        var viewButtonsDisplayed = false;
2443
2444
        var $attach,
2445
            $burnAfterReading,
2446
            $burnAfterReadingOption,
2447
            $cloneButton,
2448
            $customAttachment,
2449
            $expiration,
2450
            $fileRemoveButton,
2451
            $fileWrap,
2452
            $formatter,
2453
            $newButton,
2454
            $openDiscussion,
2455
            $openDiscussionOption,
2456
            $password,
2457
            $passwordInput,
2458
            $rawTextButton,
2459
            $sendButton;
2460
2461
        var pasteExpiration = '1week';
2462
2463
        /**
2464
         * set the expiration on bootstrap templates in dropdown
2465
         *
2466
         * @name   TopNav.updateExpiration
2467
         * @private
2468
         * @function
2469
         * @param  {Event} event
2470
         */
2471
        function updateExpiration(event)
2472
        {
2473
            // get selected option
2474
            var target = $(event.target);
2475
2476
            // update dropdown display and save new expiration time
2477
            $('#pasteExpirationDisplay').text(target.text());
2478
            pasteExpiration = target.data('expiration');
2479
2480
            event.preventDefault();
2481
        }
2482
2483
        /**
2484
         * set the format on bootstrap templates in dropdown
2485
         *
2486
         * @name   TopNav.updateFormat
2487
         * @private
2488
         * @function
2489
         * @param  {Event} event
2490
         */
2491
        function updateFormat(event)
2492
        {
2493
            // get selected option
2494
            var $target = $(event.target);
2495
2496
            // update dropdown display and save new format
2497
            var newFormat = $target.data('format');
2498
            $('#pasteFormatterDisplay').text($target.text());
2499
            PasteViewer.setFormat(newFormat);
2500
2501
            // update preview
2502
            if (Editor.isPreview()) {
2503
                PasteViewer.run();
2504
            }
2505
2506
            event.preventDefault();
2507
        }
2508
2509
        /**
2510
         * when "burn after reading" is checked, disable discussion
2511
         *
2512
         * @name   TopNav.changeBurnAfterReading
2513
         * @private
2514
         * @function
2515
         */
2516
        function changeBurnAfterReading()
2517
        {
2518
            if ($burnAfterReading.is(':checked')) {
2519
                $openDiscussionOption.addClass('buttondisabled');
2520
                $openDiscussion.prop('checked', false);
2521
2522
                // if button is actually disabled, force-enable it and uncheck other button
2523
                $burnAfterReadingOption.removeClass('buttondisabled');
2524
            } else {
2525
                $openDiscussionOption.removeClass('buttondisabled');
2526
            }
2527
        }
2528
2529
        /**
2530
         * when discussion is checked, disable "burn after reading"
2531
         *
2532
         * @name   TopNav.changeOpenDiscussion
2533
         * @private
2534
         * @function
2535
         */
2536
        function changeOpenDiscussion()
2537
        {
2538
            if ($openDiscussion.is(':checked')) {
2539
                $burnAfterReadingOption.addClass('buttondisabled');
2540
                $burnAfterReading.prop('checked', false);
2541
2542
                // if button is actually disabled, force-enable it and uncheck other button
2543
                $openDiscussionOption.removeClass('buttondisabled');
2544
            } else {
2545
                $burnAfterReadingOption.removeClass('buttondisabled');
2546
            }
2547
        }
2548
2549
        /**
2550
         * return raw text
2551
         *
2552
         * @name   TopNav.rawText
2553
         * @private
2554
         * @function
2555
         * @param  {Event} event
2556
         */
2557
        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...
2558
        {
2559
            TopNav.hideAllButtons();
2560
            Alert.showLoading('Showing raw text…', 0, 'time');
2561
            var paste = PasteViewer.getText();
2562
2563
            // push a new state to allow back navigation with browser back button
2564
            history.pushState(
2565
                {type: 'raw'},
2566
                document.title,
2567
                // recreate paste URL
2568
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2569
                Model.getPasteKey()
2570
            );
2571
2572
            // we use text/html instead of text/plain to avoid a bug when
2573
            // reloading the raw text view (it reverts to type text/html)
2574
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2575
            var newDoc = document.open('text/html', 'replace');
2576
            newDoc.write('<!DOCTYPE html><html><head>');
2577
            for (var i = 0; i < $head.length; i++) {
2578
                newDoc.write($head[i].outerHTML);
2579
            }
2580
            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
2581
            newDoc.close();
2582
        }
2583
2584
        /**
2585
         * saves the language in a cookie and reloads the page
2586
         *
2587
         * @name   TopNav.setLanguage
2588
         * @private
2589
         * @function
2590
         * @param  {Event} event
2591
         */
2592
        function setLanguage(event)
2593
        {
2594
            document.cookie = 'lang=' + $(event.target).data('lang');
2595
            UiHelper.reloadHome();
2596
        }
2597
2598
        /**
2599
         * hides all messages and creates a new paste
2600
         *
2601
         * @name   TopNav.clickNewPaste
2602
         * @private
2603
         * @function
2604
         * @param  {Event} event
2605
         */
2606
        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...
2607
        {
2608
            Controller.hideStatusMessages();
2609
            Controller.newPaste();
2610
        }
2611
2612
        /**
2613
         * removes the existing attachment
2614
         *
2615
         * @name   TopNav.removeAttachment
2616
         * @private
2617
         * @function
2618
         * @param  {Event} event
2619
         */
2620
        function removeAttachment(event)
2621
        {
2622
            // if custom attachment is used, remove it first
2623
            if (!$customAttachment.hasClass('hidden')) {
2624
                AttachmentViewer.removeAttachment();
2625
                $customAttachment.addClass('hidden');
2626
                $fileWrap.removeClass('hidden');
2627
            }
2628
2629
            // our up-to-date jQuery can handle it :)
2630
            $fileWrap.find('input').val('');
2631
2632
            // pevent '#' from appearing in the URL
2633
            event.preventDefault();
2634
        }
2635
2636
        /**
2637
         * Shows all elements belonging to viwing an existing pastes
2638
         *
2639
         * @name   TopNav.showViewButtons
2640
         * @function
2641
         */
2642
        me.showViewButtons = function()
2643
        {
2644
            if (viewButtonsDisplayed) {
2645
                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...
2646
                return;
2647
            }
2648
2649
            $newButton.removeClass('hidden');
2650
            $cloneButton.removeClass('hidden');
2651
            $rawTextButton.removeClass('hidden');
2652
2653
            viewButtonsDisplayed = true;
2654
        }
2655
2656
        /**
2657
         * Hides all elements belonging to existing pastes
2658
         *
2659
         * @name   TopNav.hideViewButtons
2660
         * @function
2661
         */
2662
        me.hideViewButtons = function()
2663
        {
2664
            if (!viewButtonsDisplayed) {
2665
                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...
2666
                return;
2667
            }
2668
2669
            $newButton.addClass('hidden');
2670
            $cloneButton.addClass('hidden');
2671
            $rawTextButton.addClass('hidden');
2672
2673
            viewButtonsDisplayed = false;
2674
        }
2675
2676
        /**
2677
         * Hides all elements belonging to existing pastes
2678
         *
2679
         * @name   TopNav.hideAllButtons
2680
         * @function
2681
         */
2682
        me.hideAllButtons = function()
2683
        {
2684
            me.hideViewButtons();
2685
            me.hideCreateButtons();
2686
        }
2687
2688
        /**
2689
         * shows all elements needed when creating a new paste
2690
         *
2691
         * @name   TopNav.showCreateButtons
2692
         * @function
2693
         */
2694
        me.showCreateButtons = function()
2695
        {
2696
            if (createButtonsDisplayed) {
2697
                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...
2698
                return;
2699
            }
2700
2701
            $sendButton.removeClass('hidden');
2702
            $expiration.removeClass('hidden');
2703
            $formatter.removeClass('hidden');
2704
            $burnAfterReadingOption.removeClass('hidden');
2705
            $openDiscussionOption.removeClass('hidden');
2706
            $newButton.removeClass('hidden');
2707
            $password.removeClass('hidden');
2708
            $attach.removeClass('hidden');
2709
2710
            createButtonsDisplayed = true;
2711
        }
2712
2713
        /**
2714
         * shows all elements needed when creating a new paste
2715
         *
2716
         * @name   TopNav.hideCreateButtons
2717
         * @function
2718
         */
2719
        me.hideCreateButtons = function()
2720
        {
2721
            if (!createButtonsDisplayed) {
2722
                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...
2723
                return;
2724
            }
2725
2726
            $newButton.addClass('hidden');
2727
            $sendButton.addClass('hidden');
2728
            $expiration.addClass('hidden');
2729
            $formatter.addClass('hidden');
2730
            $burnAfterReadingOption.addClass('hidden');
2731
            $openDiscussionOption.addClass('hidden');
2732
            $password.addClass('hidden');
2733
            $attach.addClass('hidden');
2734
2735
            createButtonsDisplayed = false;
2736
        }
2737
2738
        /**
2739
         * only shows the "new paste" button
2740
         *
2741
         * @name   TopNav.showNewPasteButton
2742
         * @function
2743
         */
2744
        me.showNewPasteButton = function()
2745
        {
2746
            $newButton.removeClass('hidden');
2747
        }
2748
2749
        /**
2750
         * only hides the clone button
2751
         *
2752
         * @name   TopNav.hideCloneButton
2753
         * @function
2754
         */
2755
        me.hideCloneButton = function()
2756
        {
2757
            $cloneButton.addClass('hidden');
2758
        }
2759
2760
        /**
2761
         * only hides the raw text button
2762
         *
2763
         * @name   TopNav.hideRawButton
2764
         * @function
2765
         */
2766
        me.hideRawButton = function()
2767
        {
2768
            $rawTextButton.addClass('hidden');
2769
        }
2770
2771
        /**
2772
         * hides the file selector in attachment
2773
         *
2774
         * @name   TopNav.hideFileSelector
2775
         * @function
2776
         */
2777
        me.hideFileSelector = function()
2778
        {
2779
            $fileWrap.addClass('hidden');
2780
        }
2781
2782
2783
        /**
2784
         * shows the custom attachment
2785
         *
2786
         * @name   TopNav.showCustomAttachment
2787
         * @function
2788
         */
2789
        me.showCustomAttachment = function()
2790
        {
2791
            $customAttachment.removeClass('hidden');
2792
        }
2793
2794
        /**
2795
         * collapses the navigation bar if nedded
2796
         *
2797
         * @name   TopNav.collapseBar
2798
         * @function
2799
         */
2800
        me.collapseBar = function()
2801
        {
2802
            var $bar = $('.navbar-toggle');
2803
2804
            // check if bar is expanded
2805
            if ($bar.hasClass('collapse in')) {
2806
                // if so, toggle it
2807
                $bar.click();
2808
            }
2809
        }
2810
2811
        /**
2812
         * returns the currently set expiration time
2813
         *
2814
         * @name   TopNav.getExpiration
2815
         * @function
2816
         * @return {int}
2817
         */
2818
        me.getExpiration = function()
2819
        {
2820
            return pasteExpiration;
2821
        }
2822
2823
        /**
2824
         * returns the currently selected file(s)
2825
         *
2826
         * @name   TopNav.getFileList
2827
         * @function
2828
         * @return {FileList|null}
2829
         */
2830
        me.getFileList = function()
2831
        {
2832
            var $file = $('#file');
2833
2834
            // if no file given, return null
2835
            if (!$file.length || !$file[0].files.length) {
2836
                return null;
2837
            }
2838
            // @TODO is this really necessary
2839
            if (!($file[0].files && $file[0].files[0])) {
2840
                return null;
2841
            }
2842
2843
            return $file[0].files;
2844
        }
2845
2846
        /**
2847
         * returns the state of the burn after reading checkbox
2848
         *
2849
         * @name   TopNav.getExpiration
2850
         * @function
2851
         * @return {bool}
2852
         */
2853
        me.getBurnAfterReading = function()
2854
        {
2855
            return $burnAfterReading.is(':checked');
2856
        }
2857
2858
        /**
2859
         * returns the state of the discussion checkbox
2860
         *
2861
         * @name   TopNav.getOpenDiscussion
2862
         * @function
2863
         * @return {bool}
2864
         */
2865
        me.getOpenDiscussion = function()
2866
        {
2867
            return $openDiscussion.is(':checked');
2868
        }
2869
2870
        /**
2871
         * returns the entered password
2872
         *
2873
         * @name   TopNav.getPassword
2874
         * @function
2875
         * @return {string}
2876
         */
2877
        me.getPassword = function()
2878
        {
2879
            return $passwordInput.val();
2880
        }
2881
2882
        /**
2883
         * returns the element where custom attachments can be placed
2884
         *
2885
         * Used by AttachmentViewer when an attachment is cloned here.
2886
         *
2887
         * @name   TopNav.getCustomAttachment
2888
         * @function
2889
         * @return {jQuery}
2890
         */
2891
        me.getCustomAttachment = function()
2892
        {
2893
            return $customAttachment;
2894
        }
2895
2896
        /**
2897
         * init navigation manager
2898
         *
2899
         * preloads jQuery elements
2900
         *
2901
         * @name   TopNav.init
2902
         * @function
2903
         */
2904
        me.init = function()
2905
        {
2906
            $attach = $('#attach');
2907
            $burnAfterReading = $('#burnafterreading');
2908
            $burnAfterReadingOption = $('#burnafterreadingoption');
2909
            $cloneButton = $('#clonebutton');
2910
            $customAttachment = $('#customattachment');
2911
            $expiration = $('#expiration');
2912
            $fileRemoveButton = $('#fileremovebutton');
2913
            $fileWrap = $('#filewrap');
2914
            $formatter = $('#formatter');
2915
            $newButton = $('#newbutton');
2916
            $openDiscussion = $('#opendiscussion');
2917
            $openDiscussionOption = $('#opendiscussionoption');
2918
            $password = $('#password');
2919
            $passwordInput = $('#passwordinput');
2920
            $rawTextButton = $('#rawtextbutton');
2921
            $sendButton = $('#sendbutton');
2922
2923
            // bootstrap template drop down
2924
            $('#language ul.dropdown-menu li a').click(setLanguage);
2925
            // page template drop down
2926
            $('#language select option').click(setLanguage);
2927
2928
            // bind events
2929
            $burnAfterReading.change(changeBurnAfterReading);
2930
            $openDiscussionOption.change(changeOpenDiscussion);
2931
            $newButton.click(clickNewPaste);
2932
            $sendButton.click(PasteEncrypter.sendPaste);
2933
            $cloneButton.click(Controller.clonePaste);
2934
            $rawTextButton.click(rawText);
2935
            $fileRemoveButton.click(removeAttachment);
2936
2937
            // bootstrap template drop downs
2938
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2939
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2940
2941
            // initiate default state of checkboxes
2942
            changeBurnAfterReading();
2943
            changeOpenDiscussion();
2944
2945
            // get default value from template or fall back to set value
2946
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2947
        }
2948
2949
        return me;
2950
    })(window, document);
2951
2952
    /**
2953
     * Responsible for AJAX requests, transparently handles encryption…
2954
     *
2955
     * @name   Uploader
2956
     * @class
2957
     */
2958
    var Uploader = (function () {
2959
        var me = {};
2960
2961
        var successFunc = null,
2962
            failureFunc = null,
2963
            url,
2964
            data,
2965
            symmetricKey,
2966
            password;
2967
2968
        /**
2969
         * public variable ('constant') for errors to prevent magic numbers
2970
         *
2971
         * @name   Uploader.error
2972
         * @readonly
2973
         * @enum   {Object}
2974
         */
2975
        me.error = {
2976
            okay: 0,
2977
            custom: 1,
2978
            unknown: 2,
2979
            serverError: 3
2980
        };
2981
2982
        /**
2983
         * ajaxHeaders to send in AJAX requests
2984
         *
2985
         * @name   Uploader.ajaxHeaders
2986
         * @private
2987
         * @readonly
2988
         * @enum   {Object}
2989
         */
2990
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2991
2992
        /**
2993
         * called after successful upload
2994
         *
2995
         * @name   Uploader.checkCryptParameters
2996
         * @private
2997
         * @function
2998
         * @throws {string}
2999
         */
3000
        function checkCryptParameters()
3001
        {
3002
            // workaround for this nasty 'bug' in ECMAScript
3003
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3004
            var typeOfKey = typeof symmetricKey;
3005
            if (symmetricKey === null) {
3006
                typeOfKey = 'null';
3007
            }
3008
3009
            // in case of missing preparation, throw error
3010
            switch (typeOfKey) {
3011
                case 'string':
3012
                    // already set, all right
3013
                    return;
3014
                case 'null':
3015
                    // needs to be generated auto-generate
3016
                    symmetricKey = CryptTool.getSymmetricKey();
3017
                    break;
3018
                default:
3019
                    console.error('current invalid symmetricKey:', symmetricKey);
3020
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3021
            }
3022
            // password is optional
3023
        }
3024
3025
        /**
3026
         * called after successful upload
3027
         *
3028
         * @name   Uploader.success
3029
         * @private
3030
         * @function
3031
         * @param {int} status
3032
         * @param {int} result - optional
3033
         */
3034
        function success(status, result)
3035
        {
3036
            // add useful data to result
3037
            result.encryptionKey = symmetricKey;
3038
            result.requestData = data;
3039
3040
            if (successFunc !== null) {
3041
                successFunc(status, result);
3042
            }
3043
        }
3044
3045
        /**
3046
         * called after a upload failure
3047
         *
3048
         * @name   Uploader.fail
3049
         * @private
3050
         * @function
3051
         * @param {int} status - internal code
3052
         * @param {int} result - original error code
3053
         */
3054
        function fail(status, result)
3055
        {
3056
            if (failureFunc !== null) {
3057
                failureFunc(status, result);
3058
            }
3059
        }
3060
3061
        /**
3062
         * actually uploads the data
3063
         *
3064
         * @name   Uploader.run
3065
         * @function
3066
         */
3067
        me.run = function()
3068
        {
3069
            $.ajax({
3070
                type: 'POST',
3071
                url: url,
3072
                data: data,
3073
                dataType: 'json',
3074
                headers: ajaxHeaders,
3075
                success: function(result) {
3076
                    if (result.status === 0) {
3077
                        success(0, result);
3078
                    } else if (result.status === 1) {
3079
                        fail(1, result);
3080
                    } else {
3081
                        fail(2, result);
3082
                    }
3083
                }
3084
            })
3085
            .fail(function(jqXHR, textStatus, errorThrown) {
3086
                console.error(textStatus, errorThrown);
3087
                fail(3, jqXHR);
3088
            });
3089
        }
3090
3091
        /**
3092
         * set success function
3093
         *
3094
         * @name   Uploader.setUrl
3095
         * @function
3096
         * @param {function} newUrl
3097
         */
3098
        me.setUrl = function(newUrl)
3099
        {
3100
            url = newUrl;
3101
        }
3102
3103
        /**
3104
         * sets the password to use (first value) and optionally also the
3105
         * encryption key (not recommend, it is automatically generated).
3106
         *
3107
         * Note: Call this after prepare() as prepare() resets these values.
3108
         *
3109
         * @name   Uploader.setCryptValues
3110
         * @function
3111
         * @param {string} newPassword
3112
         * @param {string} newKey       - optional
3113
         */
3114
        me.setCryptParameters = function(newPassword, newKey)
3115
        {
3116
            password = newPassword;
3117
3118
            if (typeof newKey !== 'undefined') {
3119
                symmetricKey = newKey;
3120
            }
3121
        }
3122
3123
        /**
3124
         * set success function
3125
         *
3126
         * @name   Uploader.setSuccess
3127
         * @function
3128
         * @param {function} func
3129
         */
3130
        me.setSuccess = function(func)
3131
        {
3132
            successFunc = func;
3133
        }
3134
3135
        /**
3136
         * set failure function
3137
         *
3138
         * @name   Uploader.setFailure
3139
         * @function
3140
         * @param {function} func
3141
         */
3142
        me.setFailure = function(func)
3143
        {
3144
            failureFunc = func;
3145
        }
3146
3147
        /**
3148
         * prepares a new upload
3149
         *
3150
         * Call this when doing a new upload to reset any data from potential
3151
         * previous uploads. Must be called before any other method of this
3152
         * module.
3153
         *
3154
         * @name   Uploader.prepare
3155
         * @function
3156
         * @return {object}
3157
         */
3158
        me.prepare = function()
3159
        {
3160
            // entropy should already be checked!
3161
3162
            // reset password
3163
            password = '';
3164
3165
            // reset key, so it a new one is generated when it is used
3166
            symmetricKey = null;
3167
3168
            // reset data
3169
            successFunc = null;
3170
            failureFunc = null;
3171
            url = Helper.baseUri();
3172
            data = {};
3173
        }
3174
3175
        /**
3176
         * encrypts and sets the data
3177
         *
3178
         * @name   Uploader.setData
3179
         * @function
3180
         * @param {string} index
3181
         * @param {mixed} element
3182
         */
3183
        me.setData = function(index, element)
3184
        {
3185
            checkCryptParameters();
3186
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3187
        }
3188
3189
        /**
3190
         * set the additional metadata to send unencrypted
3191
         *
3192
         * @name   Uploader.setUnencryptedData
3193
         * @function
3194
         * @param {string} index
3195
         * @param {mixed} element
3196
         */
3197
        me.setUnencryptedData = function(index, element)
3198
        {
3199
            data[index] = element;
3200
        }
3201
3202
        /**
3203
         * set the additional metadata to send unencrypted passed at once
3204
         *
3205
         * @name   Uploader.setUnencryptedData
3206
         * @function
3207
         * @param {object} newData
3208
         */
3209
        me.setUnencryptedBulkData = function(newData)
3210
        {
3211
            $.extend(data, newData);
3212
        }
3213
3214
        /**
3215
         * Helper, which parses shows a general error message based on the result of the Uploader
3216
         *
3217
         * @name    Uploader.parseUploadError
3218
         * @function
3219
         * @param {int} status
3220
         * @param {object} data
3221
         * @param {string} doThisThing - a human description of the action, which was tried
3222
         * @return {array}
3223
         */
3224
        me.parseUploadError = function(status, data, doThisThing) {
3225
            var errorArray;
3226
3227
            switch (status) {
3228
                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...
3229
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3230
                    break;
3231
                case me.error['unknown']:
3232
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3233
                    break;
3234
                case me.error['serverError']:
3235
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3236
                    break;
3237
                default:
3238
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3239
                    break;
3240
            }
3241
3242
            return errorArray;
3243
        }
3244
3245
        /**
3246
         * init Uploader
3247
         *
3248
         * @name   Uploader.init
3249
         * @function
3250
         */
3251
        me.init = function()
3252
        {
3253
            // nothing yet
3254
        }
3255
3256
        return me;
3257
    })();
3258
3259
    /**
3260
     * (controller) Responsible for encrypting paste and sending it to server.
3261
     *
3262
     * Does upload, encryption is done transparently by Uploader.
3263
     *
3264
     * @name PasteEncrypter
3265
     * @class
3266
     */
3267
    var PasteEncrypter = (function () {
3268
        var me = {};
3269
3270
        var requirementsChecked = false;
3271
3272
        /**
3273
         * checks whether there is a suitable amount of entrophy
3274
         *
3275
         * @name PasteEncrypter.checkRequirements
3276
         * @private
3277
         * @function
3278
         * @param {function} retryCallback - the callback to execute to retry the upload
3279
         * @return {bool}
3280
         */
3281
        function checkRequirements(retryCallback) {
3282
            // skip double requirement checks
3283
            if (requirementsChecked === true) {
3284
                return true;
3285
            }
3286
3287
            if (!CryptTool.isEntropyReady()) {
3288
                // display a message and wait
3289
                Alert.showStatus('Please move your mouse for more entropy…');
3290
3291
                CryptTool.addEntropySeedListener(retryCallback);
3292
                return false;
3293
            }
3294
3295
            requirementsChecked = true;
3296
3297
            return true;
3298
        }
3299
3300
        /**
3301
         * called after successful paste upload
3302
         *
3303
         * @name PasteEncrypter.showCreatedPaste
3304
         * @private
3305
         * @function
3306
         * @param {int} status
3307
         * @param {object} data
3308
         */
3309
        function showCreatedPaste(status, data) {
3310
            Alert.hideLoading();
3311
3312
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3313
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3314
3315
            Alert.hideMessages();
3316
3317
            // show notification
3318
            PasteStatus.createPasteNotification(url, deleteUrl)
3319
3320
            // show new URL in browser bar
3321
            history.pushState({type: 'newpaste'}, document.title, url);
3322
3323
            TopNav.showViewButtons();
3324
            TopNav.hideRawButton();
3325
            Editor.hide();
3326
3327
            // parse and show text
3328
            // (preparation already done in me.sendPaste())
3329
            PasteViewer.run();
3330
        }
3331
3332
        /**
3333
         * called after successful comment upload
3334
         *
3335
         * @name PasteEncrypter.showUploadedComment
3336
         * @private
3337
         * @function
3338
         * @param {int} status
3339
         * @param {object} data
3340
         */
3341
        function showUploadedComment(status, data) {
3342
            // show success message
3343
            // Alert.showStatus('Comment posted.');
3344
3345
            // reload paste
3346
            Controller.refreshPaste(function () {
3347
                // highlight sent comment
3348
                DiscussionViewer.highlightComment(data.id, true);
3349
                // reset error handler
3350
                Alert.setCustomHandler(null);
3351
            });
3352
        }
3353
3354
        /**
3355
         * adds attachments to the Uploader
3356
         *
3357
         * @name PasteEncrypter.encryptAttachments
3358
         * @private
3359
         * @function
3360
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3361
         * @param {function} callback - excuted when action is successful
3362
         */
3363
        function encryptAttachments(file, callback) {
3364
            if (typeof file !== 'undefined' && file !== null) {
3365
                // check file reader requirements for upload
3366
                if (typeof FileReader === 'undefined') {
3367
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3368
                    // cancels process as it does not execute callback
3369
                    return;
3370
                }
3371
3372
                var reader = new FileReader();
3373
3374
                // closure to capture the file information
3375
                reader.onload = function(event) {
3376
                    Uploader.setData('attachment', event.target.result);
3377
                    Uploader.setData('attachmentname', file.name);
3378
3379
                    // run callback
3380
                    return callback();
3381
                }
3382
3383
                // actually read first file
3384
                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...
3385
            } else if (AttachmentViewer.hasAttachment()) {
3386
                // fall back to cloned part
3387
                var attachment = AttachmentViewer.getAttachment();
3388
3389
                Uploader.setData('attachment', attachment[0]);
3390
                Uploader.setData('attachmentname', attachment[1]);
3391
                return callback();
3392
            } else {
3393
                // if there are no attachments, this is of course still successful
3394
                return callback();
3395
            }
3396
        }
3397
3398
        /**
3399
         * send a reply in a discussion
3400
         *
3401
         * @name   PasteEncrypter.sendComment
3402
         * @function
3403
         */
3404
        me.sendComment = function()
3405
        {
3406
            Alert.hideMessages();
3407
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3408
3409
            // UI loading state
3410
            TopNav.hideAllButtons();
3411
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3412
3413
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3414
            var replyData = DiscussionViewer.getReplyData(),
3415
                plainText = replyData[0],
3416
                nickname = replyData[1],
3417
                parentid = DiscussionViewer.getReplyCommentId();
3418
3419
            // do not send if there is no data
3420
            if (plainText.length === 0) {
3421
                // revert loading status…
3422
                Alert.hideLoading();
3423
                Alert.setCustomHandler(null);
3424
                TopNav.showViewButtons();
3425
                return;
3426
            }
3427
3428
            // check entropy
3429
            if (!checkRequirements(function () {
3430
                me.sendComment();
3431
            })) {
3432
                return; // to prevent multiple executions
3433
            }
3434
            Alert.showLoading(null, 10);
3435
3436
            // prepare Uploader
3437
            Uploader.prepare();
3438
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3439
3440
            // set success/fail functions
3441
            Uploader.setSuccess(showUploadedComment);
3442
            Uploader.setFailure(function (status, data) {
3443
                // revert loading status…
3444
                Alert.hideLoading();
3445
                TopNav.showViewButtons();
3446
3447
                // show error message
3448
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3449
3450
                // reset error handler
3451
                Alert.setCustomHandler(null);
3452
            });
3453
3454
            // fill it with unencrypted params
3455
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3456
            if (typeof parentid === 'undefined') {
3457
                // if parent id is not set, this is the top-most comment, so use
3458
                // paste id as parent @TODO is this really good?
3459
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3460
            } else {
3461
                Uploader.setUnencryptedData('parentid', parentid);
3462
            }
3463
3464
            // encrypt data
3465
            Uploader.setData('data', plainText);
3466
3467
            if (nickname.length > 0) {
3468
                Uploader.setData('nickname', nickname);
3469
            }
3470
3471
            Uploader.run();
3472
        }
3473
3474
        /**
3475
         * sends a new paste to server
3476
         *
3477
         * @name   PasteEncrypter.sendPaste
3478
         * @function
3479
         */
3480
        me.sendPaste = function()
3481
        {
3482
            // hide previous (error) messages
3483
            Controller.hideStatusMessages();
3484
3485
            // UI loading state
3486
            TopNav.hideAllButtons();
3487
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3488
            TopNav.collapseBar();
3489
3490
            // get data
3491
            var plainText = Editor.getText(),
3492
                format = PasteViewer.getFormat(),
3493
                files = TopNav.getFileList();
3494
3495
            // do not send if there is no data
3496
            if (plainText.length === 0 && files === null) {
3497
                // revert loading status…
3498
                Alert.hideLoading();
3499
                TopNav.showCreateButtons();
3500
                return;
3501
            }
3502
3503
            Alert.showLoading(null, 10);
3504
3505
            // check entropy
3506
            if (!checkRequirements(function () {
3507
                me.sendPaste();
3508
            })) {
3509
                return; // to prevent multiple executions
3510
            }
3511
3512
            // prepare Uploader
3513
            Uploader.prepare();
3514
            Uploader.setCryptParameters(TopNav.getPassword());
3515
3516
            // set success/fail functions
3517
            Uploader.setSuccess(showCreatedPaste);
3518
            Uploader.setFailure(function (status, data) {
3519
                // revert loading status…
3520
                Alert.hideLoading();
3521
                TopNav.showCreateButtons();
3522
3523
                // show error message
3524
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3525
            });
3526
3527
            // fill it with unencrypted submitted options
3528
            Uploader.setUnencryptedBulkData({
3529
                expire:           TopNav.getExpiration(),
3530
                formatter:        format,
3531
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3532
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3533
            });
3534
3535
            // prepare PasteViewer for later preview
3536
            PasteViewer.setText(plainText);
3537
            PasteViewer.setFormat(format);
3538
3539
            // encrypt cipher data
3540
            Uploader.setData('data', plainText);
3541
3542
            // encrypt attachments
3543
            encryptAttachments(
3544
                files === null ? null : files[0],
3545
                function () {
3546
                    // send data
3547
                    Uploader.run();
3548
                }
3549
            );
3550
        }
3551
3552
        /**
3553
         * initialize
3554
         *
3555
         * @name   PasteEncrypter.init
3556
         * @function
3557
         */
3558
        me.init = function()
3559
        {
3560
            // nothing yet
3561
        }
3562
3563
        return me;
3564
    })();
3565
3566
    /**
3567
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3568
     *
3569
     * Only decryption, no download.
3570
     *
3571
     * @name PasteDecrypter
3572
     * @class
3573
     */
3574
    var PasteDecrypter = (function () {
3575
        var me = {};
3576
3577
        /**
3578
         * decrypt data or prompts for password in cvase of failure
3579
         *
3580
         * @name   PasteDecrypter.decryptOrPromptPassword
3581
         * @private
3582
         * @function
3583
         * @param  {string} key
3584
         * @param  {string} password - optional, may be an empty string
3585
         * @param  {string} cipherdata
3586
         * @throws {string}
3587
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3588
         */
3589
        function decryptOrPromptPassword(key, password, cipherdata)
3590
        {
3591
            // try decryption without password
3592
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3593
3594
            // if it fails, request password
3595
            if (plaindata.length === 0 && password.length === 0) {
3596
                // try to get cached password first
3597
                password = Prompt.getPassword();
3598
3599
                // if password is there, re-try
3600
                if (password.length === 0) {
3601
                    password = Prompt.requestPassword();
3602
                }
3603
                // recursive
3604
                // note: an infinite loop is prevented as the previous if
3605
                // clause checks whether a password is already set and ignores
3606
                // errors when a password has been passed
3607
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3608
            }
3609
3610
            // if all tries failed, we can only return an error
3611
            if (plaindata.length === 0) {
3612
                throw 'failed to decipher data';
3613
            }
3614
3615
            return plaindata;
3616
        }
3617
3618
        /**
3619
         * decrypt the actual paste text
3620
         *
3621
         * @name   PasteDecrypter.decryptOrPromptPassword
3622
         * @private
3623
         * @function
3624
         * @param  {object} paste - paste data in object form
3625
         * @param  {string} key
3626
         * @param  {string} password
3627
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3628
         * @return {bool} whether action was successful
3629
         * @throws {string}
3630
         */
3631
        function decryptPaste(paste, key, password, ignoreError)
3632
        {
3633
            var plaintext
3634
            if (ignoreError === true) {
3635
                plaintext = CryptTool.decipher(key, password, paste.data);
3636
            } else {
3637
                try {
3638
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3639
                } catch (err) {
3640
                    throw 'failed to decipher paste text: ' + err
3641
                }
3642
                if (plaintext === false) {
3643
                    return false;
3644
                }
3645
            }
3646
3647
            // on success show paste
3648
            PasteViewer.setFormat(paste.meta.formatter);
3649
            PasteViewer.setText(plaintext);
3650
            // trigger to show the text (attachment loaded afterwards)
3651
            PasteViewer.run();
3652
3653
            return true;
3654
        }
3655
3656
        /**
3657
         * decrypts any attachment
3658
         *
3659
         * @name   PasteDecrypter.decryptAttachment
3660
         * @private
3661
         * @function
3662
         * @param  {object} paste - paste data in object form
3663
         * @param  {string} key
3664
         * @param  {string} password
3665
         * @return {bool} whether action was successful
3666
         * @throws {string}
3667
         */
3668
        function decryptAttachment(paste, key, password)
3669
        {
3670
            // decrypt attachment
3671
            try {
3672
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3673
            } catch (err) {
3674
                throw 'failed to decipher attachment: ' + err
3675
            }
3676
            if (attachment === false) {
3677
                return false;
3678
            }
3679
3680
            // decrypt attachment name
3681
            var attachmentName;
3682
            if (paste.attachmentname) {
3683
                try {
3684
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3685
                } catch (err) {
3686
                    throw 'failed to decipher attachment name: ' + err
3687
                }
3688
                if (attachmentName === false) {
3689
                    return false;
3690
                }
3691
            }
3692
3693
            AttachmentViewer.setAttachment(attachment, attachmentName);
0 ignored issues
show
The variable attachmentName does not seem to be initialized in case paste.attachmentname on line 3682 is false. Are you sure the function setAttachment handles undefined variables?
Loading history...
3694
            AttachmentViewer.showAttachment();
3695
3696
            return true;
3697
        }
3698
3699
        /**
3700
         * decrypts all comments and shows them
3701
         *
3702
         * @name   PasteDecrypter.decryptComments
3703
         * @private
3704
         * @function
3705
         * @param  {object} paste - paste data in object form
3706
         * @param  {string} key
3707
         * @param  {string} password
3708
         * @return {bool} whether action was successful
3709
         */
3710
        function decryptComments(paste, key, password)
3711
        {
3712
            // remove potentially previous discussion
3713
            DiscussionViewer.prepareNewDisucssion();
3714
3715
            // iterate over comments
3716
            for (var i = 0; i < paste.comments.length; ++i) {
3717
                var comment = paste.comments[i];
3718
3719
                DiscussionViewer.addComment(
3720
                    comment,
3721
                    CryptTool.decipher(key, password, comment.data),
3722
                    CryptTool.decipher(key, password, comment.meta.nickname)
3723
                );
3724
            }
3725
3726
            DiscussionViewer.finishDiscussion();
3727
            DiscussionViewer.showDiscussion();
3728
            return true;
3729
        }
3730
3731
        /**
3732
         * show decrypted text in the display area, including discussion (if open)
3733
         *
3734
         * @name   PasteDecrypter.run
3735
         * @function
3736
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3737
         */
3738
        me.run = function(paste)
3739
        {
3740
            Alert.hideMessages();
3741
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3742
3743
            if (typeof paste === 'undefined') {
3744
                paste = $.parseJSON(Model.getCipherData());
3745
            }
3746
3747
            var key = Model.getPasteKey(),
3748
                password = Prompt.getPassword();
3749
3750
            if (PasteViewer.isPrettyPrinted()) {
3751
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3752
                return;
3753
            }
3754
3755
            // try to decrypt the paste
3756
            try {
3757
                // decrypt attachments
3758
                if (paste.attachment) {
3759
                    // try to decrypt paste and if it fails (because the password is
3760
                    // missing) return to let JS continue and wait for user
3761
                    if (!decryptAttachment(paste, key, password)) {
3762
                        return;
3763
                    }
3764
                    // ignore empty paste, as this is allowed when pasting attachments
3765
                    decryptPaste(paste, key, password, true);
3766
                } else {
3767
                    decryptPaste(paste, key, password);
3768
                }
3769
3770
3771
                // shows the remaining time (until) deletion
3772
                PasteStatus.showRemainingTime(paste.meta);
3773
3774
                // if the discussion is opened on this paste, display it
3775
                if (paste.meta.opendiscussion) {
3776
                    decryptComments(paste, key, password);
3777
                }
3778
3779
                Alert.hideLoading();
3780
                TopNav.showViewButtons();
3781
            } catch(err) {
3782
                Alert.hideLoading();
3783
3784
                // log and show error
3785
                console.error(err);
3786
                Alert.showError('Could not decrypt data (Wrong key?)');
3787
            }
3788
        }
3789
3790
        /**
3791
         * initialize
3792
         *
3793
         * @name   PasteDecrypter.init
3794
         * @function
3795
         */
3796
        me.init = function()
3797
        {
3798
            // nothing yet
3799
        }
3800
3801
        return me;
3802
    })();
3803
3804
    /**
3805
     * (controller) main PrivateBin logic
3806
     *
3807
     * @name   Controller
3808
     * @param  {object} window
3809
     * @param  {object} document
3810
     * @class
3811
     */
3812
    var Controller = (function (window, document) {
3813
        var me = {};
3814
3815
        /**
3816
         * hides all status messages no matter which module showed them
3817
         *
3818
         * @name   Controller.hideStatusMessages
3819
         * @function
3820
         */
3821
        me.hideStatusMessages = function()
3822
        {
3823
            PasteStatus.hideMessages();
3824
            Alert.hideMessages();
3825
        }
3826
3827
        /**
3828
         * creates a new paste
3829
         *
3830
         * @name   Controller.newPaste
3831
         * @function
3832
         */
3833
        me.newPaste = function()
3834
        {
3835
            // Important: This *must not* run Alert.hideMessages() as previous
3836
            // errors from viewing a paste should be shown.
3837
            TopNav.hideAllButtons();
3838
            Alert.showLoading('Preparing new paste…', 0, 'time');
3839
3840
            PasteStatus.hideMessages();
3841
            PasteViewer.hide();
3842
            Editor.resetInput();
3843
            Editor.show();
3844
            Editor.focusInput();
3845
3846
            TopNav.showCreateButtons();
3847
            Alert.hideLoading();
3848
        }
3849
3850
        /**
3851
         * shows the loaded paste
3852
         *
3853
         * @name   Controller.showPaste
3854
         * @function
3855
         */
3856
        me.showPaste = function()
3857
        {
3858
            try {
3859
                Model.getPasteId();
3860
                Model.getPasteKey();
3861
            } catch (err) {
3862
                console.error(err);
3863
3864
                // missing decryption key (or paste ID) in URL?
3865
                if (window.location.hash.length === 0) {
3866
                    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?)');
3867
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3868
                    return;
3869
                }
3870
            }
3871
3872
            // show proper elements on screen
3873
            PasteDecrypter.run();
3874
        }
3875
3876
        /**
3877
         * refreshes the loaded paste to show potential new data
3878
         *
3879
         * @name   Controller.refreshPaste
3880
         * @function
3881
         * @param  {function} callback
3882
         */
3883
        me.refreshPaste = function(callback)
3884
        {
3885
            // save window position to restore it later
3886
            var orgPosition = $(window).scrollTop();
3887
3888
            Uploader.prepare();
3889
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3890
3891
            Uploader.setFailure(function (status, data) {
3892
                // revert loading status…
3893
                Alert.hideLoading();
3894
                TopNav.showViewButtons();
3895
3896
                // show error message
3897
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3898
            })
3899
            Uploader.setSuccess(function (status, data) {
3900
                PasteDecrypter.run(data);
3901
3902
                // restore position
3903
                window.scrollTo(0, orgPosition);
3904
3905
                callback();
3906
            })
3907
            Uploader.run();
3908
        }
3909
3910
        /**
3911
         * clone the current paste
3912
         *
3913
         * @name   Controller.clonePaste
3914
         * @function
3915
         * @param  {Event} event
3916
         */
3917
        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...
3918
        {
3919
            TopNav.collapseBar();
3920
            TopNav.hideAllButtons();
3921
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3922
3923
            // hide messages from previous paste
3924
            me.hideStatusMessages();
3925
3926
            // erase the id and the key in url
3927
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3928
3929
            if (AttachmentViewer.hasAttachment()) {
3930
                AttachmentViewer.moveAttachmentTo(
3931
                    TopNav.getCustomAttachment(),
3932
                    'Cloned: \'%s\''
3933
                );
3934
                TopNav.hideFileSelector();
3935
                AttachmentViewer.hideAttachment();
3936
                // NOTE: it also looks nice without removing the attachment
3937
                // but for a consistent display we remove it…
3938
                AttachmentViewer.hideAttachmentPreview();
3939
                TopNav.showCustomAttachment();
3940
3941
                // show another status message to make the user aware that the
3942
                // file was cloned too!
3943
                Alert.showStatus(
3944
                    [
3945
                        'The cloned file \'%s\' was attached to this paste.',
3946
                        AttachmentViewer.getAttachment()[1]
3947
                    ], 'copy', true, true);
3948
            }
3949
3950
            Editor.setText(PasteViewer.getText())
3951
            PasteViewer.hide();
3952
            Editor.show();
3953
3954
            Alert.hideLoading();
3955
            TopNav.showCreateButtons();
3956
        }
3957
3958
        /**
3959
         * removes a saved paste
3960
         *
3961
         * @name   Controller.removePaste
3962
         * @function
3963
         * @param  {string} pasteId
3964
         * @param  {string} deleteToken
3965
         */
3966
        me.removePaste = function(pasteId, deleteToken) {
3967
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3968
            // so we use a POST request
3969
            Uploader.prepare();
3970
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3971
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3972
3973
            Uploader.setFailure(function () {
3974
                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3975
            })
3976
            Uploader.run();
3977
        }
3978
3979
        /**
3980
         * application start
3981
         *
3982
         * @name   Controller.init
3983
         * @function
3984
         */
3985
        me.init = function()
3986
        {
3987
            // first load translations
3988
            I18n.loadTranslations();
3989
3990
            // initialize other modules/"classes"
3991
            Alert.init();
3992
            Model.init();
3993
3994
            AttachmentViewer.init();
3995
            DiscussionViewer.init();
3996
            Editor.init();
3997
            PasteDecrypter.init();
3998
            PasteEncrypter.init();
3999
            PasteStatus.init();
4000
            PasteViewer.init();
4001
            Prompt.init();
4002
            TopNav.init();
4003
            UiHelper.init();
4004
            Uploader.init();
4005
4006
            // display an existing paste
4007
            if (Model.hasCipherData()) {
4008
                return me.showPaste();
4009
            }
4010
4011
            // otherwise create a new paste
4012
            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...
4013
        }
4014
4015
        return me;
4016
    })(window, document);
4017
4018
    return {
4019
        Helper: Helper,
4020
        I18n: I18n,
4021
        CryptTool: CryptTool,
4022
        Model: Model,
4023
        UiHelper: UiHelper,
4024
        Alert: Alert,
4025
        PasteStatus: PasteStatus,
4026
        Prompt: Prompt,
4027
        Editor: Editor,
4028
        PasteViewer: PasteViewer,
4029
        AttachmentViewer: AttachmentViewer,
4030
        DiscussionViewer: DiscussionViewer,
4031
        TopNav: TopNav,
4032
        Uploader: Uploader,
4033
        PasteEncrypter: PasteEncrypter,
4034
        PasteDecrypter: PasteDecrypter,
4035
        Controller: Controller
4036
    };
4037
}(jQuery, sjcl, Base64, RawDeflate);
4038