Test Failed
Pull Request — master (#180)
by El
06:05
created

Helper.constructor   B

Complexity

Conditions 1
Paths 2

Size

Total Lines 271

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 2
nop 0
dl 0
loc 271
rs 8.2857
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @class
40
     */
41
    var Helper = (function () {
42
        var me = {};
43
44
        /**
45
         * character to HTML entity lookup table
46
         *
47
         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
48
         * @private
49
         * @enum   {Object}
50
         * @readonly
51
         */
52
        var entityMap = {
53
            '&': '&',
54
            '<': '&lt;',
55
            '>': '&gt;',
56
            '"': '&quot;',
57
            "'": '&#39;',
58
            '/': '&#x2F;',
59
            '`': '&#x60;',
60
            '=': '&#x3D;'
61
        };
62
63
        /**
64
         * cache for script location
65
         *
66
         * @private
67
         * @enum   {string|null}
68
         */
69
        var baseUri = null;
70
71
        /**
72
         * converts a duration (in seconds) into human friendly approximation
73
         *
74
         * @name Helper.secondsToHuman
75
         * @function
76
         * @param  {number} seconds
77
         * @return {Array}
78
         */
79
        me.secondsToHuman = function(seconds)
80
        {
81
            var v;
82
            if (seconds < 60)
83
            {
84
                v = Math.floor(seconds);
85
                return [v, 'second'];
86
            }
87
            if (seconds < 60 * 60)
88
            {
89
                v = Math.floor(seconds / 60);
90
                return [v, 'minute'];
91
            }
92
            if (seconds < 60 * 60 * 24)
93
            {
94
                v = Math.floor(seconds / (60 * 60));
95
                return [v, 'hour'];
96
            }
97
            // If less than 2 months, display in days:
98
            if (seconds < 60 * 60 * 24 * 60)
99
            {
100
                v = Math.floor(seconds / (60 * 60 * 24));
101
                return [v, 'day'];
102
            }
103
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
104
            return [v, 'month'];
105
        }
106
107
        /**
108
         * checks if a string is valid text (and not onyl whitespace)
109
         *
110
         * @name Helper.isValidText
111
         * @function
112
         * @param  {string} string
113
         * @return {bool}
114
         */
115
        me.isValidText = function(string)
116
        {
117
            return (string.length > 0 && $.trim(string) !== '')
118
        }
119
120
        /**
121
         * text range selection
122
         *
123
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
124
         * @name   Helper.selectText
125
         * @function
126
         * @param  {HTMLElement} element
127
         */
128
        me.selectText = function(element)
129
        {
130
            var range, selection;
131
132
            // MS
133
            if (document.body.createTextRange) {
134
                range = document.body.createTextRange();
135
                range.moveToElementText(element);
136
                range.select();
137
            } else if (window.getSelection){
138
                selection = window.getSelection();
139
                range = document.createRange();
140
                range.selectNodeContents(element);
141
                selection.removeAllRanges();
142
                selection.addRange(range);
143
            }
144
        }
145
146
        /**
147
         * set text of a jQuery element (required for IE),
148
         *
149
         * @name   Helper.setElementText
150
         * @function
151
         * @param  {jQuery} $element - a jQuery element
152
         * @param  {string} text - the text to enter
153
         */
154
        me.setElementText = function($element, text)
155
        {
156
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
157
            if ($('#oldienotice').is(':visible')) {
158
                var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
159
                $element.html('<pre>' + html + '</pre>');
160
            }
161
            // for other (sane) browsers:
162
            else
163
            {
164
                $element.text(text);
165
            }
166
        }
167
168
        /**
169
         * convert URLs to clickable links.
170
         * URLs to handle:
171
         * <pre>
172
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
173
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
174
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
175
         * </pre>
176
         *
177
         * @name   Helper.urls2links
178
         * @function
179
         * @param  {Object} element - a jQuery DOM element
0 ignored issues
show
Documentation Bug introduced by
The parameter element does not exist. Did you maybe mean $element instead?
Loading history...
180
         */
181
        me.urls2links = function($element)
182
        {
183
            var markup = '<a href="$1" rel="nofollow">$1</a>';
184
            $element.html(
185
                $element.html().replace(
186
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
187
                    markup
188
                )
189
            );
190
            $element.html(
191
                $element.html().replace(
192
                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
193
                    markup
194
                )
195
            );
196
        }
197
198
        /**
199
         * minimal sprintf emulation for %s and %d formats
200
         *
201
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
202
         * @name   Helper.sprintf
203
         * @function
204
         * @param  {string} format
0 ignored issues
show
Documentation introduced by
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
205
         * @param  {...*} args - one or multiple parameters injected into format string
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
206
         * @return {string}
207
         */
208
        me.sprintf = function()
209
        {
210
            var args = Array.prototype.slice.call(arguments);
211
            var format = args[0],
212
                i = 1;
213
            return format.replace(/%((%)|s|d)/g, function (m) {
214
                // m is the matched format, e.g. %s, %d
215
                var val;
216
                if (m[2]) {
217
                    val = m[2];
218
                } else {
219
                    val = args[i];
220
                    // A switch statement so that the formatter can be extended.
221
                    switch (m)
222
                    {
223
                        case '%d':
224
                            val = parseFloat(val);
225
                            if (isNaN(val)) {
226
                                val = 0;
227
                            }
228
                            break;
229
                        default:
230
                            // Default is %s
231
                    }
232
                    ++i;
233
                }
234
                return val;
235
            });
236
        }
237
238
        /**
239
         * get value of cookie, if it was set, empty string otherwise
240
         *
241
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
242
         * @name   Helper.getCookie
243
         * @function
244
         * @param  {string} cname
245
         * @return {string}
246
         */
247
        me.getCookie = function(cname) {
248
            var name = cname + '=',
249
                ca = document.cookie.split(';');
250
            for (var i = 0; i < ca.length; ++i) {
251
                var c = ca[i];
252
                while (c.charAt(0) === ' ')
253
                {
254
                    c = c.substring(1);
255
                }
256
                if (c.indexOf(name) === 0)
257
                {
258
                    return c.substring(name.length, c.length);
259
                }
260
            }
261
            return '';
262
        }
263
264
        /**
265
         * get the current location (without search or hash part of the URL),
266
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
267
         *
268
         * @name   Helper.baseUri
269
         * @function
270
         * @return {string}
271
         */
272
        me.baseUri = function()
273
        {
274
            // check for cached version
275
            if (baseUri !== null) {
276
                return baseUri;
277
            }
278
279
            baseUri = window.location.origin + window.location.pathname;
280
            return baseUri;
281
        }
282
283
        /**
284
         * convert all applicable characters to HTML entities
285
         *
286
         * @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}
287
         * @name   Helper.htmlEntities
288
         * @function
289
         * @param  {string} str
290
         * @return {string} escaped HTML
291
         */
292
        me.htmlEntities = function(str) {
293
            return String(str).replace(
294
                /[&<>"'`=\/]/g, function(s) {
295
                    return entityMap[s];
296
                });
297
        }
298
299
        /**
300
         * resets state, used for unit testing
301
         *
302
         * @name   Model.reset
303
         * @function
304
         */
305
        me.reset = function()
306
        {
307
            baseUri = null;
308
        }
309
310
        return me;
311
    })();
312
313
    /**
314
     * internationalization module
315
     *
316
     * @param  {object} window
317
     * @param  {object} document
318
     * @class
319
     */
320
    var I18n = (function (window, document) {
321
        var me = {};
322
323
        /**
324
         * const for string of loaded language
325
         *
326
         * @private
327
         * @prop   {string}
328
         * @readonly
329
         */
330
        var languageLoadedEvent = 'languageLoaded';
331
332
        /**
333
         * supported languages, minus the built in 'en'
334
         *
335
         * @private
336
         * @prop   {string[]}
337
         * @readonly
338
         */
339
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
340
341
        /**
342
         * built in language
343
         *
344
         * @private
345
         * @prop   {string|null}
346
         */
347
        var language = null;
348
349
        /**
350
         * translation cache
351
         *
352
         * @private
353
         * @enum   {Object}
354
         */
355
        var translations = {};
356
357
        /**
358
         * translate a string, alias for I18n.translate()
359
         *
360
         * for a full description see me.translate
361
         *
362
         * @name   I18n._
363
         * @function
364
         * @param  {jQuery} $element - optional
0 ignored issues
show
Documentation introduced by
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
365
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
366
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
367
         * @return {string}
368
         */
369
        me._ = function()
370
        {
371
            return me.translate.apply(this, arguments);
372
        }
373
374
        /**
375
         * translate a string
376
         *
377
         * Optionally pass a jQuery element as the first parameter, to automatically
378
         * let the text of this element be replaced. In case the (asynchronously
379
         * loaded) language is not downloadet yet, this will make sure the string
380
         * is replaced when it is actually loaded.
381
         * So for easy translations passing the jQuery object to apply it to is
382
         * more save, especially when they are loaded in the beginning.
383
         *
384
         * @name   I18n.translate
385
         * @function
386
         * @param  {jQuery} $element - optional
0 ignored issues
show
Documentation introduced by
The parameter $element does not exist. Did you maybe forget to remove this comment?
Loading history...
387
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
388
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
389
         * @return {string}
390
         */
391
        me.translate = function()
392
        {
393
            // convert parameters to array
394
            var args = Array.prototype.slice.call(arguments),
395
                messageId,
396
                $element = null;
397
398
            // parse arguments
399
            if (args[0] instanceof jQuery) {
400
                // optional jQuery element as first parameter
401
                $element = args[0];
402
                args.shift();
403
            }
404
405
            // extract messageId from arguments
406
            var usesPlurals = $.isArray(args[0]);
407
            if (usesPlurals) {
408
                // use the first plural form as messageId, otherwise the singular
409
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
410
            } else {
411
                messageId = args[0];
412
            }
413
414
            if (messageId.length === 0) {
415
                return messageId;
416
            }
417
418
            // if no translation string cannot be found (in translations object)
419
            if (!translations.hasOwnProperty(messageId)) {
420
                // if language is still loading and we have an elemt assigned
421
                if (language === null && $element !== null) {
422
                    // handle the error by attaching the language loaded event
423
                    var orgArguments = arguments;
424
                    $(document).on(languageLoadedEvent, function () {
425
                        // log to show that the previous error could be mitigated
426
                        console.log('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
427
                        // re-execute this function
428
                        me.translate.apply(this, orgArguments);
429
                    });
430
431
                    // and fall back to English for now until the real language
432
                    // file is loaded
433
                }
434
435
                // for all other langauges than English for which thsi behaviour
436
                // is expected as it is built-in, log error
437
                if (language !== 'en') {
438
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
439
                    // fallback to English
440
                }
441
442
                // save English translation (should be the same on both sides)
443
                translations[messageId] = args[0];
444
            }
445
446
            // lookup plural translation
447
            if (usesPlurals && $.isArray(translations[messageId])) {
448
                var n = parseInt(args[1] || 1, 10),
449
                    key = me.getPluralForm(n),
450
                    maxKey = translations[messageId].length - 1;
451
                if (key > maxKey) {
452
                    key = maxKey;
453
                }
454
                args[0] = translations[messageId][key];
455
                args[1] = n;
456
            } else {
457
                // lookup singular translation
458
                args[0] = translations[messageId];
459
            }
460
461
            // format string
462
            var output = Helper.sprintf.apply(this, args);
463
464
            // if $element is given, apply text to element
465
            if ($element !== null) {
466
                $element.text(output);
467
            }
468
469
            return output;
470
        }
471
472
        /**
473
         * per language functions to use to determine the plural form
474
         *
475
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
476
         * @name   I18n.getPluralForm
477
         * @function
478
         * @param  {int} n
479
         * @return {int} array key
480
         */
481
        me.getPluralForm = function(n) {
482
            switch (language)
483
            {
484
                case 'fr':
485
                case 'oc':
486
                case 'zh':
487
                    return (n > 1 ? 1 : 0);
488
                case 'pl':
489
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
490
                case 'ru':
491
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
492
                case 'sl':
493
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
494
                // de, en, es, it, no, pt
495
                default:
496
                    return (n !== 1 ? 1 : 0);
497
            }
498
        }
499
500
        /**
501
         * load translations into cache
502
         *
503
         * @name   I18n.loadTranslations
504
         * @function
505
         */
506
        me.loadTranslations = function()
507
        {
508
            var newLanguage = Helper.getCookie('lang');
509
510
            // auto-select language based on browser settings
511
            if (newLanguage.length === 0) {
512
                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
513
            }
514
515
            // if language is already used skip update
516
            if (newLanguage === language) {
517
                return;
518
            }
519
520
            // if language is built-in (English) skip update
521
            if (newLanguage === 'en') {
522
                language = 'en';
523
                return;
524
            }
525
526
            // if language is not supported, show error
527
            if (supportedLanguages.indexOf(newLanguage) === -1) {
528
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
529
                language = 'en';
530
                return;
531
            }
532
533
            // load strings from JSON
534
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
535
                language = newLanguage;
536
                translations = data;
537
                $(document).triggerHandler(languageLoadedEvent);
538
            }).fail(function (data, textStatus, errorMsg) {
539
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
540
                language = 'en';
541
            });
542
        }
543
544
        return me;
545
    })(window, document);
546
547
    /**
548
     * handles everything related to en/decryption
549
     *
550
     * @class
551
     */
552
    var CryptTool = (function () {
553
        var me = {};
554
555
        /**
556
         * compress a message (deflate compression), returns base64 encoded data
557
         *
558
         * @name   cryptToolcompress
559
         * @function
560
         * @private
561
         * @param  {string} message
562
         * @return {string} base64 data
563
         */
564
        function compress(message)
565
        {
566
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
567
        }
568
569
        /**
570
         * decompress a message compressed with cryptToolcompress()
571
         *
572
         * @name   cryptTooldecompress
573
         * @function
574
         * @private
575
         * @param  {string} data - base64 data
576
         * @return {string} message
577
         */
578
        function decompress(data)
579
        {
580
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
581
        }
582
583
        /**
584
         * compress, then encrypt message with given key and password
585
         *
586
         * @name   CryptTool.cipher
587
         * @function
588
         * @param  {string} key
589
         * @param  {string} password
590
         * @param  {string} message
591
         * @return {string} data - JSON with encrypted data
592
         */
593
        me.cipher = function(key, password, message)
594
        {
595
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
596
            var options = {
597
                mode: 'gcm',
598
                ks: 256,
599
                ts: 128
600
            };
601
602
            if ((password || '').trim().length === 0) {
603
                return sjcl.encrypt(key, compress(message), options);
604
            }
605
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
606
        }
607
608
        /**
609
         * decrypt message with key, then decompress
610
         *
611
         * @name   CryptTool.decipher
612
         * @function
613
         * @param  {string} key
614
         * @param  {string} password
615
         * @param  {string} data - JSON with encrypted data
616
         * @return {string} decrypted message
617
         */
618
        me.decipher = function(key, password, data)
619
        {
620
            if (data !== undefined) {
621
                try {
622
                    return decompress(sjcl.decrypt(key, data));
623
                } catch(err) {
624
                    try {
625
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
626
                    } catch(e) {
627
                        // ignore error, because ????? @TODO
628
                    }
629
                }
630
            }
631
            return '';
632
        }
633
634
        /**
635
         * checks whether the crypt tool is ready.
636
         *
637
         * @name   CryptTool.isReady
638
         * @function
639
         * @return {bool}
640
         */
641
        me.isEntropyReady = function()
642
        {
643
            return sjcl.random.isReady();
644
        }
645
646
        /**
647
         * checks whether the crypt tool is ready.
648
         *
649
         * @name   CryptTool.isReady
650
         * @function
651
         * @param {function} func
652
         */
653
        me.addEntropySeedListener = function(func)
654
        {
655
            sjcl.random.addEventListener('seeded', func);
656
        }
657
658
        /**
659
         * returns a random symmetric key
660
         *
661
         * @name   CryptTool.getSymmetricKey
662
         * @function
663
         * @return {string} func
664
         */
665
        me.getSymmetricKey = function(func)
0 ignored issues
show
Unused Code introduced by
The parameter func 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...
666
        {
667
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
668
        }
669
670
        /**
671
         * initialize crypt tool
672
         *
673
         * @name   CryptTool.init
674
         * @function
675
         */
676
        me.init = function()
677
        {
678
            // will fail earlier as sjcl is already passed as a parameter
679
            // if (typeof sjcl !== 'object') {
680
            //     Alert.showError(
681
            //         I18n._('The library %s is not available.', 'sjcl') +
682
            //         I18n._('Messages cannot be decrypted or encrypted.')
683
            //     );
684
            // }
685
        }
686
687
        return me;
688
    })();
689
690
    /**
691
     * (Model) Data source (aka MVC)
692
     *
693
     * @class
694
     */
695
    var Model = (function () {
696
        var me = {};
697
698
        var $cipherData,
699
            $templates;
700
701
        var id = null, symmetricKey = null;
702
703
        /**
704
         * returns the expiration set in the HTML
705
         *
706
         * @name   Model.getExpirationDefault
707
         * @function
708
         * @return string
709
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
710
         */
711
        me.getExpirationDefault = function()
712
        {
713
            return $('#pasteExpiration').val();
714
        }
715
716
        /**
717
         * returns the format set in the HTML
718
         *
719
         * @name   Model.getFormatDefault
720
         * @function
721
         * @return string
722
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
723
         */
724
        me.getFormatDefault = function()
725
        {
726
            return $('#pasteFormatter').val();
727
        }
728
729
        /**
730
         * check if cipher data was supplied
731
         *
732
         * @name   Model.getCipherData
733
         * @function
734
         * @return boolean
735
         */
736
        me.hasCipherData = function()
737
        {
738
            return (me.getCipherData().length > 0);
739
        }
740
741
        /**
742
         * returns the cipher data
743
         *
744
         * @name   Model.getCipherData
745
         * @function
746
         * @return string
747
         */
748
        me.getCipherData = function()
749
        {
750
            return $cipherData.text();
751
        }
752
753
        /**
754
         * get the pastes unique identifier from the URL,
755
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
756
         *
757
         * @name   Model.getPasteId
758
         * @function
759
         * @return {string} unique identifier
760
         * @throws {string}
761
         */
762
        me.getPasteId = function()
763
        {
764
            if (id === null) {
765
                id = window.location.search.substring(1);
766
767
                if (id === '') {
768
                    throw 'no paste id given';
769
                }
770
            }
771
772
            return id;
773
        }
774
775
        /**
776
         * return the deciphering key stored in anchor part of the URL
777
         *
778
         * @name   Model.getPasteKey
779
         * @function
780
         * @return {string|null} key
781
         * @throws {string}
782
         */
783
        me.getPasteKey = function()
784
        {
785
            if (symmetricKey === null) {
786
                symmetricKey = window.location.hash.substring(1);
787
788
                if (symmetricKey === '') {
789
                    throw 'no encryption key given';
790
                }
791
792
                // Some web 2.0 services and redirectors add data AFTER the anchor
793
                // (such as &utm_source=...). We will strip any additional data.
794
                var ampersandPos = symmetricKey.indexOf('&');
795
                if (ampersandPos > -1)
796
                {
797
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
798
                }
799
            }
800
801
            return symmetricKey;
802
        }
803
804
        /**
805
         * returns a jQuery copy of the HTML template
806
         *
807
         * @name Model.getTemplate
808
         * @function
809
         * @param  {string} name - the name of the template
810
         * @return {jQuery}
811
         */
812
        me.getTemplate = function(name)
813
        {
814
            // find template
815
            var $element = $templates.find('#' + name + 'template').clone(true);
816
            // change ID to avoid collisions (one ID should really be unique)
817
            return $element.prop('id', name);
818
        }
819
820
        /**
821
         * resets state, used for unit testing
822
         *
823
         * @name   Model.reset
824
         * @function
825
         */
826
        me.reset = function()
827
        {
828
            $cipherData = $templates = id = symmetricKey = null;
829
        }
830
831
832
        /**
833
         * init navigation manager
834
         *
835
         * preloads jQuery elements
836
         *
837
         * @name   Model.init
838
         * @function
839
         */
840
        me.init = function()
841
        {
842
            $cipherData = $('#cipherdata');
843
            $templates = $('#templates');
844
        }
845
846
        return me;
847
    })();
848
849
    /**
850
     * Helper functions for user interface
851
     *
852
     * everything directly UI-related, which fits nowhere else
853
     *
854
     * @param  {object} window
855
     * @param  {object} document
856
     * @class
857
     */
858
    var UiHelper = (function (window, document) {
859
        var me = {};
860
861
        /**
862
         * handle history (pop) state changes
863
         *
864
         * currently this does only handle redirects to the home page.
865
         *
866
         * @private
867
         * @function
868
         * @param  {Event} event
869
         */
870
        function historyChange(event)
871
        {
872
            var currentLocation = Helper.baseUri();
873
            if (event.originalEvent.state === null && // no state object passed
874
                event.originalEvent.target.location.href === currentLocation && // target location is home page
875
                window.location.href === currentLocation // and we are not already on the home page
876
            ) {
877
                // redirect to home page
878
                window.location.href = currentLocation;
879
            }
880
        }
881
882
        /**
883
         * reload the page
884
         *
885
         * This takes the user to the PrivateBin homepage.
886
         *
887
         * @name   UiHelper.reloadHome
888
         * @function
889
         */
890
        me.reloadHome = function()
891
        {
892
            window.location.href = Helper.baseUri();
893
        }
894
895
        /**
896
         * checks whether the element is currently visible in the viewport (so
897
         * the user can actually see it)
898
         *
899
         * THanks to https://stackoverflow.com/a/40658647
900
         *
901
         * @name   UiHelper.isVisible
902
         * @function
903
         * @param  {jQuery} $element The link hash to move to.
904
         */
905
        me.isVisible = function($element)
906
        {
907
            var elementTop = $element.offset().top;
908
            var elementBottom = elementTop + $element.outerHeight();
0 ignored issues
show
Unused Code introduced by
The variable elementBottom seems to be never used. Consider removing it.
Loading history...
909
910
            var viewportTop = $(window).scrollTop();
911
            var viewportBottom = viewportTop + $(window).height();
912
913
            return (elementTop > viewportTop && elementTop < viewportBottom);
914
        }
915
916
        /**
917
         * scrolls to a specific element
918
         *
919
         * Based on code by @hanoo: https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767
920
         *
921
         * @name   UiHelper.scrollTo
922
         * @param  {jQuery}           $element        The link hash to move to.
923
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
924
         * @param  {string}           animationEffect   passed to jQuery .animate
925
         * @param  {function}         finishedCallback  function to call after animation finished
926
         */
927
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
928
        {
929
            var $body = $('html, body'),
930
                margin = 50,
931
                callbackCalled = false;
932
933
            //calculate destination place
934
            var dest = 0;
935
            // if it would scroll out of the screen at the bottom only scroll it as
936
            // far as the screen can go
937
            if ($element.offset().top > $(document).height() - $(window).height()) {
938
                dest = $(document).height() - $(window).height();
939
            } else {
940
                dest = $element.offset().top - margin;
941
            }
942
            // skip animation if duration is set to 0
943
            if (animationDuration === 0) {
944
                window.scrollTo(0, dest);
945
            } else {
946
                // stop previous animation
947
                $body.stop();
948
                // scroll to destination
949
                $body.animate({
950
                    scrollTop: dest
951
                }, animationDuration, animationEffect);
952
            }
953
954
            // as we have finished we can enable scrolling again
955
            $body.queue(function (next) {
956
                if (!callbackCalled) {
957
                    // call user function if needed
958
                    if (typeof finishedCallback !== 'undefined') {
959
                        finishedCallback();
960
                    }
961
962
                    // prevent calling this function twice
963
                    callbackCalled = true;
964
                }
965
                next();
966
            });
967
        }
968
969
        /**
970
         * initialize
971
         *
972
         * @name   UiHelper.init
973
         * @function
974
         */
975
        me.init = function()
976
        {
977
            // update link to home page
978
            $('.reloadlink').prop('href', Helper.baseUri());
979
980
            $(window).on('popstate', historyChange);
981
        }
982
983
        return me;
984
    })(window, document);
985
986
    /**
987
     * Alert/error manager
988
     *
989
     * @param  {object} window
990
     * @param  {object} document
991
     * @class
992
     */
993
    var Alert = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window 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...
Unused Code introduced by
The parameter document 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...
994
        var me = {};
995
996
        var $errorMessage,
997
            $loadingIndicator,
998
            $statusMessage,
999
            $remainingTime;
1000
1001
        var currentIcon = [
1002
            'glyphicon-time', // loading icon
1003
            'glyphicon-info-sign', // status icon
1004
            '', // resevered for warning, not used yet
1005
            'glyphicon-alert' // error icon
1006
        ];
1007
1008
        var alertType = [
1009
            'loading', // not in bootstrap, but using a good value here
1010
            'info', // status icon
1011
            'warning', // not used yet
1012
            'danger' // error icon
1013
        ];
1014
1015
        var customHandler;
1016
1017
        /**
1018
         * forwards a request to the i18n module and shows the element
1019
         *
1020
         * @private
1021
         * @function
1022
         * @param  {int} id - id of notification
1023
         * @param  {jQuery} $element - jQuery object
1024
         * @param  {string|array} args
1025
         * @param  {string|null} icon - optional, icon
1026
         */
1027
        function handleNotification(id, $element, args, icon)
1028
        {
1029
            // basic parsing/conversion of parameters
1030
            if (typeof icon === 'undefined') {
1031
                icon = null;
1032
            }
1033
            if (typeof args === 'undefined') {
1034
                args = null;
1035
            } else if (typeof args === 'string') {
1036
                // convert string to array if needed
1037
                args = [args];
1038
            }
1039
1040
            // pass to custom handler if defined
1041
            if (typeof customHandler === 'function') {
1042
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1043
                if (handlerResult === true) {
1044
                    // if it returs true, skip own handler
1045
                    return;
1046
                }
1047
                if (handlerResult instanceof jQuery) {
1048
                    // continue processing with new element
1049
                    $element = handlerResult;
1050
                    icon = null; // icons not supported in this case
1051
                }
1052
            }
1053
1054
            // handle icon
1055
            if (icon !== null && // icon was passed
1056
                icon !== currentIcon[id] // and it differs from current icon
1057
            ) {
1058
                var $glyphIcon = $element.find(':first');
1059
1060
                // remove (previous) icon
1061
                $glyphIcon.removeClass(currentIcon[id]);
1062
1063
                // any other thing as a string (e.g. 'null') (only) removes the icon
1064
                if (typeof icon === 'string') {
1065
                    // set new icon
1066
                    currentIcon[id] = 'glyphicon-' + icon;
1067
                    $glyphIcon.addClass(currentIcon[id]);
1068
                }
1069
            }
1070
1071
            // show text
1072
            if (args !== null) {
1073
                // get last text node of element
1074
                var content = $element.contents();
1075
                if (content.length > 1) {
1076
                    content[content.length - 1].nodeValue = ' ' + I18n._(args);
1077
                } else {
1078
                    $element.text(I18n._(args));
1079
                }
1080
            }
1081
1082
            // show notification
1083
            $element.removeClass('hidden');
1084
        }
1085
1086
        /**
1087
         * display a status message
1088
         *
1089
         * This automatically passes the text to I18n for translation.
1090
         *
1091
         * @name   Alert.showStatus
1092
         * @function
1093
         * @param  {string|array} message     string, use an array for %s/%d options
1094
         * @param  {string|null}  icon        optional, the icon to show,
1095
         *                                    default: leave previous icon
1096
         * @param  {bool}         dismissable optional, whether the notification
1097
         *                                    can be dismissed (closed), default: false
1098
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1099
         *                                    notification should be hidden automatically;
1100
         *                                    default: disabled (0); use true for default value
1101
         */
1102
        me.showStatus = function(message, icon, dismissable, autoclose)
0 ignored issues
show
Unused Code introduced by
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...
Unused Code introduced by
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...
1103
        {
1104
            console.log('status shown: ', message);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1105
            // @TODO: implement dismissable
1106
            // @TODO: implement autoclose
1107
1108
            handleNotification(1, $statusMessage, message, icon);
1109
        }
1110
1111
        /**
1112
         * display an error message
1113
         *
1114
         * This automatically passes the text to I18n for translation.
1115
         *
1116
         * @name   Alert.showError
1117
         * @function
1118
         * @param  {string|array} message     string, use an array for %s/%d options
1119
         * @param  {string|null}  icon        optional, the icon to show, default:
1120
         *                                    leave previous icon
1121
         * @param  {bool}         dismissable optional, whether the notification
1122
         *                                    can be dismissed (closed), default: false
1123
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1124
         *                                    notification should be hidden automatically;
1125
         *                                    default: disabled (0); use true for default value
1126
         */
1127
        me.showError = function(message, icon, dismissable, autoclose)
0 ignored issues
show
Unused Code introduced by
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...
Unused Code introduced by
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...
1128
        {
1129
            console.error('error message shown: ', message);
1130
            // @TODO: implement dismissable (bootstrap add-on has it)
1131
            // @TODO: implement autoclose
1132
1133
            handleNotification(3, $errorMessage, message, icon);
1134
        }
1135
1136
        /**
1137
         * display remaining message
1138
         *
1139
         * This automatically passes the text to I18n for translation.
1140
         *
1141
         * @name   Alert.showRemaining
1142
         * @function
1143
         * @param  {string|array} message     string, use an array for %s/%d options
1144
         */
1145
        me.showRemaining = function(message)
1146
        {
1147
            console.error('remaining message shown: ', message);
1148
            handleNotification(1, $remainingTime, message);
1149
        }
1150
1151
        /**
1152
         * shows a loading message, optionally with a percentage
1153
         *
1154
         * This automatically passes all texts to the i10s module.
1155
         *
1156
         * @name   Alert.showLoading
1157
         * @function
1158
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1159
         * @param  {int}               percentage   optional, default: null
1160
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1161
         */
1162
        me.showLoading = function(message, percentage, icon)
1163
        {
1164
            if (typeof message !== 'undefined' && message !== null) {
1165
                console.log('status changed: ', message);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1166
            }
1167
1168
            // default message text
1169
            if (typeof message === 'undefined') {
1170
                message = 'Loading…';
1171
            }
1172
1173
            // currently percentage parameter is ignored
1174
            // // @TODO handle it here…
1175
1176
            handleNotification(0, $loadingIndicator, message, icon);
1177
1178
            // show loading status (cursor)
1179
            $('body').addClass('loading');
1180
        }
1181
1182
        /**
1183
         * hides the loading message
1184
         *
1185
         * @name   Alert.hideLoading
1186
         * @function
1187
         */
1188
        me.hideLoading = function()
1189
        {
1190
            $loadingIndicator.addClass('hidden');
1191
1192
            // hide loading cursor
1193
            $('body').removeClass('loading');
1194
        }
1195
1196
        /**
1197
         * hides any status/error messages
1198
         *
1199
         * This does not include the loading message.
1200
         *
1201
         * @name   Alert.hideMessages
1202
         * @function
1203
         */
1204
        me.hideMessages = function()
1205
        {
1206
            // also possible: $('.statusmessage').addClass('hidden');
1207
            $statusMessage.addClass('hidden');
1208
            $errorMessage.addClass('hidden');
1209
        }
1210
1211
        /**
1212
         * set a custom handler, which gets all notifications.
1213
         *
1214
         * This handler gets the following arguments:
1215
         * alertType (see array), $element, args, icon
1216
         * If it returns true, the own processing will be stopped so the message
1217
         * will not be displayed. Otherwise it will continue.
1218
         * As an aditional feature it can return q jQuery element, which will
1219
         * then be used to add the message there. Icons are not supported in
1220
         * that case and will be ignored.
1221
         * Pass 'null' to reset/delete the custom handler.
1222
         * Note that there is no notification when a message is supposed to get
1223
         * hidden.
1224
         *
1225
         * @name   Alert.setCustomHandler
1226
         * @function
1227
         * @param {function|null} newHandler
1228
         */
1229
        me.setCustomHandler = function(newHandler)
1230
        {
1231
            customHandler = newHandler;
1232
        }
1233
1234
        /**
1235
         * init status manager
1236
         *
1237
         * preloads jQuery elements
1238
         *
1239
         * @name   Alert.init
1240
         * @function
1241
         */
1242
        me.init = function()
1243
        {
1244
            // hide "no javascript" error message
1245
            $('#noscript').hide();
1246
1247
            // not a reset, but first set of the elements
1248
            $errorMessage = $('#errormessage');
1249
            $loadingIndicator = $('#loadingindicator');
1250
            $statusMessage = $('#status');
1251
            $remainingTime = $('#remainingtime');
1252
        }
1253
1254
        return me;
1255
    })(window, document);
1256
1257
    /**
1258
     * handles paste status/result
1259
     *
1260
     * @param  {object} window
1261
     * @param  {object} document
1262
     * @class
1263
     */
1264
    var PasteStatus = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter document 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...
1265
        var me = {};
1266
1267
        var $pasteSuccess,
1268
            $pasteUrl,
1269
            $remainingTime,
1270
            $shortenButton;
1271
1272
        /**
1273
         * forward to URL shortener
1274
         *
1275
         * @private
1276
         * @function
1277
         * @param  {Event} event
1278
         */
1279
        function sendToShortener(event)
0 ignored issues
show
Unused Code introduced by
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...
1280
        {
1281
            window.location.href = $shortenButton.data('shortener')
1282
                                   + encodeURIComponent($pasteUrl.attr('href'));
1283
        }
1284
1285
        /**
1286
         * Forces opening the paste if the link does not do this automatically.
1287
         *
1288
         * This is necessary as browsers will not reload the page when it is
1289
         * already loaded (which is fake as it is set via history.pushState()).
1290
         *
1291
         * @name   Controller.pasteLinkClick
1292
         * @function
1293
         * @param  {Event} event
1294
         */
1295
        function pasteLinkClick(event)
0 ignored issues
show
Unused Code introduced by
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...
1296
        {
1297
            // check if location is (already) shown in URL bar
1298
            if (window.location.href === $pasteUrl.attr('href')) {
1299
                // if so we need to load link by reloading the current site
1300
                window.location.reload(true);
1301
            }
1302
        }
1303
1304
        /**
1305
         * creates a notification after a successfull paste upload
1306
         *
1307
         * @name   PasteStatus.createPasteNotification
1308
         * @function
1309
         * @param  {string} url
1310
         * @param  {string} deleteUrl
1311
         */
1312
        me.createPasteNotification = function(url, deleteUrl)
1313
        {
1314
            $('#pastelink').html(
1315
                I18n._(
1316
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1317
                    url, url
1318
                )
1319
            );
1320
            // save newly created element
1321
            $pasteUrl = $('#pasteurl');
1322
            // and add click event
1323
            $pasteUrl.click(pasteLinkClick);
1324
1325
            // shorten button
1326
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1327
1328
            // show result
1329
            $pasteSuccess.removeClass('hidden');
1330
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1331
            Helper.selectText($pasteUrl[0]);
1332
        }
1333
1334
        /**
1335
         * shows the remaining time
1336
         *
1337
         * @name PasteStatus.showRemainingTime
1338
         * @function
1339
         * @param {object} pasteMetaData
1340
         */
1341
        me.showRemainingTime = function(pasteMetaData)
1342
        {
1343
            if (pasteMetaData.burnafterreading) {
1344
                // display paste "for your eyes only" if it is deleted
1345
1346
                // actually remove paste, before we claim it is deleted
1347
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1348
1349
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1350
                $remainingTime.addClass('foryoureyesonly');
1351
1352
                // discourage cloning (it cannot really be prevented)
1353
                TopNav.hideCloneButton();
1354
1355
            } else if (pasteMetaData.expire_date) {
1356
                // display paste expiration
1357
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1358
                    expirationLabel = [
1359
                        'This document will expire in %d ' + expiration[1] + '.',
1360
                        'This document will expire in %d ' + expiration[1] + 's.'
1361
                    ];
1362
1363
                Alert.showRemaining(expirationLabel, expiration[0]);
1364
                $remainingTime.removeClass('foryoureyesonly')
1365
            } else {
1366
                // never expires
1367
                return;
1368
            }
1369
1370
            // in the end, display notification
1371
            $remainingTime.removeClass('hidden');
1372
        }
1373
1374
        /**
1375
         * hides the remaining time and successful upload notification
1376
         *
1377
         * @name PasteStatus.hideRemainingTime
1378
         * @function
1379
         */
1380
        me.hideMessages = function()
1381
        {
1382
            $remainingTime.addClass('hidden');
1383
            $pasteSuccess.addClass('hidden');
1384
        }
1385
1386
        /**
1387
         * init status manager
1388
         *
1389
         * preloads jQuery elements
1390
         *
1391
         * @name   Alert.init
1392
         * @function
1393
         */
1394
        me.init = function()
1395
        {
1396
            $pasteSuccess = $('#pasteSuccess');
1397
            // $pasteUrl is saved in me.createPasteNotification() after creation
1398
            $remainingTime = $('#remainingtime');
1399
            $shortenButton = $('#shortenbutton');
1400
1401
            // bind elements
1402
            $shortenButton.click(sendToShortener);
1403
        }
1404
1405
        return me;
1406
    })(window, document);
1407
1408
    /**
1409
     * password prompt
1410
     *
1411
     * @param  {object} window
1412
     * @param  {object} document
1413
     * @class
1414
     */
1415
    var Prompt = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter document 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...
Unused Code introduced by
The parameter window 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...
1416
        var me = {};
1417
1418
        var $passwordDecrypt,
1419
            $passwordForm,
1420
            $passwordModal;
1421
1422
        var password = '',
1423
            passwordCallback = null;
1424
1425
        /**
1426
         * ask the user for the password and set it
1427
         *
1428
         * the callback set via setPasswordCallback is executed
1429
         *
1430
         * @name Prompt.requestPassword()
1431
         * @function
1432
         */
1433
        me.requestPassword = function()
1434
        {
1435
            // show new bootstrap method (if available)
1436
            if ($passwordModal.length !== 0) {
1437
                $passwordModal.modal({
1438
                    backdrop: 'static',
1439
                    keyboard: false
1440
                });
1441
1442
                return;
1443
            }
1444
1445
            // fallback to old method for page template
1446
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1447
            if (newPassword === null) {
1448
                throw 'password prompt canceled';
1449
            }
1450
            if (password.length === 0) {
1451
                // recursive…
1452
                return me.requestPassword();
1453
            }
1454
1455
            password = newPassword;
1456
1457
            if (passwordCallback !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if passwordCallback !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
1458
                passwordCallback();
0 ignored issues
show
Best Practice introduced by
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...
1459
            }
1460
        }
1461
1462
        /**
1463
         * getthe cached password
1464
         *
1465
         * If you do not get a password with this function
1466
         * (returns an empty string), use requestPassword.
1467
         *
1468
         * @name   Prompt.getPassword
1469
         * @function
1470
         * @return {string}
1471
         */
1472
        me.getPassword = function()
1473
        {
1474
            return password;
1475
        }
1476
1477
        /**
1478
         * setsthe callback called when password is entered
1479
         *
1480
         * @name   Prompt.setPasswordCallback
1481
         * @function
1482
         * @param {functions} setPasswordCallback
0 ignored issues
show
Documentation introduced by
The parameter setPasswordCallback does not exist. Did you maybe forget to remove this comment?
Loading history...
1483
         */
1484
        me.setPasswordCallback = function(callback)
1485
        {
1486
            passwordCallback = callback;
1487
        }
1488
1489
        /**
1490
         * submit a password in the modal dialog
1491
         *
1492
         * @private
1493
         * @function
1494
         * @param  {Event} event
1495
         */
1496
        function submitPasswordModal(event)
1497
        {
1498
            // get input
1499
            password = $passwordDecrypt.val();
1500
1501
            // hide modal
1502
            $passwordModal.modal('hide');
1503
1504
            if (passwordCallback !== null) {
1505
                passwordCallback();
1506
            }
1507
1508
            event.preventDefault();
1509
        }
1510
1511
1512
        /**
1513
         * init status manager
1514
         *
1515
         * preloads jQuery elements
1516
         *
1517
         * @name   Controller.init
1518
         * @function
1519
         */
1520
        me.init = function()
1521
        {
1522
            $passwordDecrypt = $('#passworddecrypt');
1523
            $passwordForm = $('#passwordform');
1524
            $passwordModal = $('#passwordmodal');
1525
1526
            // bind events
1527
1528
            // focus password input when it is shown
1529
            $passwordModal.on('shown.bs.Model', function () {
1530
                $passwordDecrypt.focus();
1531
            });
1532
            // handle Model password submission
1533
            $passwordForm.submit(submitPasswordModal);
1534
        }
1535
1536
        return me;
1537
    })(window, document);
1538
1539
    /**
1540
     * Manage paste/message input, and preview tab
1541
     *
1542
     * Note that the actual preview is handled by PasteViewer.
1543
     *
1544
     * @param  {object} window
1545
     * @param  {object} document
1546
     * @class
1547
     */
1548
    var Editor = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window 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...
Unused Code introduced by
The parameter document 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...
1549
        var me = {};
1550
1551
        var $editorTabs,
1552
            $messageEdit,
1553
            $messagePreview,
1554
            $message;
1555
1556
        var isPreview = false;
1557
1558
        /**
1559
         * support input of tab character
1560
         *
1561
         * @name   Editor.supportTabs
1562
         * @function
1563
         * @param  {Event} event
1564
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1565
         */
1566
        function supportTabs(event)
1567
        {
1568
            var keyCode = event.keyCode || event.which;
1569
            // tab was pressed
1570
            if (keyCode === 9) {
1571
                // get caret position & selection
1572
                var val   = this.value,
1573
                    start = this.selectionStart,
1574
                    end   = this.selectionEnd;
1575
                // set textarea value to: text before caret + tab + text after caret
1576
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1577
                // put caret at right position again
1578
                this.selectionStart = this.selectionEnd = start + 1;
1579
                // prevent the textarea to lose focus
1580
                event.preventDefault();
1581
            }
1582
        }
1583
1584
        /**
1585
         * view the Editor tab
1586
         *
1587
         * @name   Editor.viewEditor
1588
         * @function
1589
         * @param  {Event} event - optional
1590
         */
1591
        function viewEditor(event)
1592
        {
1593
            // toggle buttons
1594
            $messageEdit.addClass('active');
1595
            $messagePreview.removeClass('active');
1596
1597
            PasteViewer.hide();
1598
1599
            // reshow input
1600
            $message.removeClass('hidden');
1601
1602
            me.focusInput();
1603
1604
            // finish
1605
            isPreview = false;
1606
1607
            // prevent jumping of page to top
1608
            if (typeof event !== 'undefined') {
1609
                event.preventDefault();
1610
            }
1611
        }
1612
1613
        /**
1614
         * view the preview tab
1615
         *
1616
         * @name   Editor.viewPreview
1617
         * @function
1618
         * @param  {Event} event
1619
         */
1620
        function viewPreview(event)
1621
        {
1622
            // toggle buttons
1623
            $messageEdit.removeClass('active');
1624
            $messagePreview.addClass('active');
1625
1626
            // hide input as now preview is shown
1627
            $message.addClass('hidden');
1628
1629
            // show preview
1630
            PasteViewer.setText($message.val());
1631
            PasteViewer.run();
1632
1633
            // finish
1634
            isPreview = true;
1635
1636
            // prevent jumping of page to top
1637
            if (typeof event !== 'undefined') {
1638
                event.preventDefault();
1639
            }
1640
        }
1641
1642
        /**
1643
         * get the state of the preview
1644
         *
1645
         * @name   Editor.isPreview
1646
         * @function
1647
         */
1648
        me.isPreview = function()
1649
        {
1650
            return isPreview;
1651
        }
1652
1653
        /**
1654
         * reset the Editor view
1655
         *
1656
         * @name   Editor.resetInput
1657
         * @function
1658
         */
1659
        me.resetInput = function()
1660
        {
1661
            // go back to input
1662
            if (isPreview) {
1663
                viewEditor();
1664
            }
1665
1666
            // clear content
1667
            $message.val('');
1668
        }
1669
1670
        /**
1671
         * shows the Editor
1672
         *
1673
         * @name   Editor.show
1674
         * @function
1675
         */
1676
        me.show = function()
1677
        {
1678
            $message.removeClass('hidden');
1679
            $editorTabs.removeClass('hidden');
1680
        }
1681
1682
        /**
1683
         * hides the Editor
1684
         *
1685
         * @name   Editor.reset
1686
         * @function
1687
         */
1688
        me.hide = function()
1689
        {
1690
            $message.addClass('hidden');
1691
            $editorTabs.addClass('hidden');
1692
        }
1693
1694
        /**
1695
         * focuses the message input
1696
         *
1697
         * @name   Editor.focusInput
1698
         * @function
1699
         */
1700
        me.focusInput = function()
1701
        {
1702
            $message.focus();
1703
        }
1704
1705
        /**
1706
         * sets a new text
1707
         *
1708
         * @name   Editor.setText
1709
         * @function
1710
         * @param {string} newText
1711
         */
1712
        me.setText = function(newText)
1713
        {
1714
            $message.val(newText);
1715
        }
1716
1717
        /**
1718
         * returns the current text
1719
         *
1720
         * @name   Editor.getText
1721
         * @function
1722
         * @return {string}
1723
         */
1724
        me.getText = function()
1725
        {
1726
            return $message.val()
1727
        }
1728
1729
        /**
1730
         * init status manager
1731
         *
1732
         * preloads jQuery elements
1733
         *
1734
         * @name   Editor.init
1735
         * @function
1736
         */
1737
        me.init = function()
1738
        {
1739
            $editorTabs = $('#editorTabs');
1740
            $message = $('#message');
1741
1742
            // bind events
1743
            $message.keydown(supportTabs);
1744
1745
            // bind click events to tab switchers (a), but save parent of them
1746
            // (li)
1747
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1748
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1749
        }
1750
1751
        return me;
1752
    })(window, document);
1753
1754
    /**
1755
     * (view) Parse and show paste.
1756
     *
1757
     * @param  {object} window
1758
     * @param  {object} document
1759
     * @class
1760
     */
1761
    var PasteViewer = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window 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...
Unused Code introduced by
The parameter document 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...
1762
        var me = {};
1763
1764
        var $placeholder,
1765
            $prettyMessage,
1766
            $prettyPrint,
1767
            $plainText;
1768
1769
        var text,
1770
            format = 'plaintext',
1771
            isDisplayed = false,
1772
            isChanged = true; // by default true as nothing was parsed yet
1773
1774
        /**
1775
         * apply the set format on paste and displays it
1776
         *
1777
         * @private
1778
         * @function
1779
         */
1780
        function parsePaste()
1781
        {
1782
            // skip parsing if no text is given
1783
            if (text === '') {
1784
                return;
1785
            }
1786
1787
            // set text
1788
            Helper.setElementText($plainText, text);
1789
            Helper.setElementText($prettyPrint, text);
1790
1791
            switch (format) {
1792
                case 'markdown':
1793
                    var converter = new showdown.Converter({
1794
                        strikethrough: true,
1795
                        tables: true,
1796
                        tablesHeaderId: true
1797
                    });
1798
                    $plainText.html(
1799
                        converter.makeHtml(text)
1800
                    );
1801
                    // add table classes from bootstrap css
1802
                    $plainText.find('table').addClass('table-condensed table-bordered');
1803
                    break;
1804
                case 'syntaxhighlighting':
1805
                    // @TODO is this really needed or is "one" enough?
1806
                    if (typeof prettyPrint === 'function')
1807
                    {
1808
                        prettyPrint();
1809
                    }
1810
1811
                    $prettyPrint.html(
1812
                        prettyPrintOne(
1813
                            Helper.htmlEntities(text), null, true
1814
                        )
1815
                    );
1816
                    // fall through, as the rest is the same
1817
                default: // = 'plaintext'
1818
                    // convert URLs to clickable links
1819
                    Helper.urls2links($plainText);
1820
                    Helper.urls2links($prettyPrint);
1821
1822
                    $prettyPrint.css('white-space', 'pre-wrap');
1823
                    $prettyPrint.css('word-break', 'normal');
1824
                    $prettyPrint.removeClass('prettyprint');
1825
            }
1826
        }
1827
1828
        /**
1829
         * displays the paste
1830
         *
1831
         * @private
1832
         * @function
1833
         */
1834
        function showPaste()
1835
        {
1836
            // instead of "nothing" better display a placeholder
1837
            if (text === '') {
1838
                $placeholder.removeClass('hidden')
1839
                return;
1840
            }
1841
            // otherwise hide the placeholder
1842
            $placeholder.addClass('hidden')
1843
1844
            switch (format) {
1845
                case 'markdown':
1846
                    $plainText.removeClass('hidden');
1847
                    $prettyMessage.addClass('hidden');
1848
                    break;
1849
                default:
1850
                    $plainText.addClass('hidden');
1851
                    $prettyMessage.removeClass('hidden');
1852
                    break;
1853
            }
1854
        }
1855
1856
        /**
1857
         * sets the format in which the text is shown
1858
         *
1859
         * @name   PasteViewer.setFormat
1860
         * @function
1861
         * @param {string} newFormat the the new format
1862
         */
1863
        me.setFormat = function(newFormat)
1864
        {
1865
            // skip if there is no update
1866
            if (format === newFormat) {
1867
                return;
1868
            }
1869
1870
            // needs to update display too, if from or to Markdown is switched
1871
            if (format === 'markdown' || newFormat === 'markdown') {
1872
                isDisplayed = false;
1873
            }
1874
1875
            format = newFormat;
1876
            isChanged = true;
1877
        }
1878
1879
        /**
1880
         * returns the current format
1881
         *
1882
         * @name   PasteViewer.setFormat
1883
         * @function
1884
         * @return {string}
1885
         */
1886
        me.getFormat = function()
1887
        {
1888
            return format;
1889
        }
1890
1891
        /**
1892
         * returns whether the current view is pretty printed
1893
         *
1894
         * @name   PasteViewer.isPrettyPrinted
1895
         * @function
1896
         * @return {bool}
1897
         */
1898
        me.isPrettyPrinted = function()
1899
        {
1900
            return $prettyPrint.hasClass('prettyprinted');
1901
        }
1902
1903
        /**
1904
         * sets the text to show
1905
         *
1906
         * @name   PasteViewer.setText
1907
         * @function
1908
         * @param {string} newText the text to show
1909
         */
1910
        me.setText = function(newText)
1911
        {
1912
            if (text !== newText) {
1913
                text = newText;
1914
                isChanged = true;
1915
            }
1916
        }
1917
1918
        /**
1919
         * gets the current cached text
1920
         *
1921
         * @name   PasteViewer.getText
1922
         * @function
1923
         * @return {string}
1924
         */
1925
        me.getText = function(newText)
0 ignored issues
show
Unused Code introduced by
The parameter newText 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...
1926
        {
1927
            return text;
1928
        }
1929
1930
        /**
1931
         * show/update the parsed text (preview)
1932
         *
1933
         * @name   PasteViewer.run
1934
         * @function
1935
         */
1936
        me.run = function()
1937
        {
1938
            if (isChanged) {
1939
                parsePaste();
1940
                isChanged = false;
1941
            }
1942
1943
            if (!isDisplayed) {
1944
                showPaste();
1945
                isDisplayed = true;
1946
            }
1947
        }
1948
1949
        /**
1950
         * hide parsed text (preview)
1951
         *
1952
         * @name   PasteViewer.hide
1953
         * @function
1954
         */
1955
        me.hide = function()
1956
        {
1957
            if (!isDisplayed) {
1958
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1959
            }
1960
1961
            $plainText.addClass('hidden');
1962
            $prettyMessage.addClass('hidden');
1963
            $placeholder.addClass('hidden');
1964
1965
            isDisplayed = false;
1966
        }
1967
1968
        /**
1969
         * init status manager
1970
         *
1971
         * preloads jQuery elements
1972
         *
1973
         * @name   Editor.init
1974
         * @function
1975
         */
1976
        me.init = function()
1977
        {
1978
            $placeholder = $('#placeholder');
1979
            $plainText = $('#plaintext');
1980
            $prettyMessage = $('#prettymessage');
1981
            $prettyPrint = $('#prettyprint');
1982
1983
            // check requirements
1984
            if (typeof prettyPrintOne !== 'function') {
1985
                Alert.showError([
1986
                    'The library %s is not available. This may cause display errors.',
1987
                    'pretty print'
1988
                ]);
1989
            }
1990
            if (typeof showdown !== 'object') {
1991
                Alert.showError([
1992
                    'The library %s is not available. This may cause display errors.',
1993
                    'showdown'
1994
                ]);
1995
            }
1996
1997
            // get default option from template/HTML or fall back to set value
1998
            format = Model.getFormatDefault() || format;
1999
        }
2000
2001
        return me;
2002
    })(window, document);
2003
2004
    /**
2005
     * (view) Show attachment and preview if possible
2006
     *
2007
     * @param  {object} window
2008
     * @param  {object} document
2009
     * @class
2010
     */
2011
    var AttachmentViewer = (function (window, document) {
2012
        var me = {};
2013
2014
        var $attachmentLink,
2015
            $attachmentPreview,
2016
            $attachment;
2017
2018
        var attachmentChanged = false,
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2019
            attachmentHasPreview = false;
2020
2021
        /**
2022
         * sets the attachment but does not yet show it
2023
         *
2024
         * @name   AttachmentViewer.setAttachment
2025
         * @function
2026
         * @param {string} attachmentData - base64-encoded data of file
2027
         * @param {string} fileName - optional, file name
2028
         */
2029
        me.setAttachment = function(attachmentData, fileName)
2030
        {
2031
            var imagePrefix = 'data:image/';
2032
2033
            $attachmentLink.attr('href', attachmentData);
2034
            if (typeof fileName !== 'undefined') {
2035
                $attachmentLink.attr('download', fileName);
2036
            }
2037
2038
            // if the attachment is an image, display it
2039
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2040
                $attachmentPreview.html(
2041
                    $(document.createElement('img'))
2042
                        .attr('src', attachmentData)
2043
                        .attr('class', 'img-thumbnail')
2044
                );
2045
                attachmentHasPreview = true;
2046
            }
2047
2048
            attachmentChanged = true;
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2049
        }
2050
2051
        /**
2052
         * displays the attachment
2053
         *
2054
         * @name AttachmentViewer.showAttachment
2055
         * @function
2056
         */
2057
        me.showAttachment = function()
2058
        {
2059
            $attachment.removeClass('hidden');
2060
2061
            if (attachmentHasPreview) {
2062
                $attachmentPreview.removeClass('hidden');
2063
            }
2064
        }
2065
2066
        /**
2067
         * removes the attachment
2068
         *
2069
         * This automatically hides the attachment containers to, to
2070
         * prevent an inconsistent display.
2071
         *
2072
         * @name AttachmentViewer.removeAttachment
2073
         * @function
2074
         */
2075
        me.removeAttachment = function()
2076
        {
2077
            me.hideAttachment();
2078
            me.hideAttachmentPreview();
2079
            $attachmentLink.prop('href', '');
2080
            $attachmentLink.prop('download', '');
2081
            $attachmentPreview.html('');
2082
        }
2083
2084
        /**
2085
         * hides the attachment
2086
         *
2087
         * This will not hide the preview {@see me.hideAttachmentPreview}
2088
         * nor will it hide the attachment link if it was moved somewhere
2089
         * else {@see moveAttachmentTo}.
2090
         *
2091
         * @name AttachmentViewer.hideAttachment
2092
         * @function
2093
         */
2094
        me.hideAttachment = function()
2095
        {
2096
            $attachment.addClass('hidden');
2097
        }
2098
2099
        /**
2100
         * hides the attachment preview
2101
         *
2102
         * @name AttachmentViewer.hideAttachmentPreview
2103
         * @function
2104
         */
2105
        me.hideAttachmentPreview = function()
2106
        {
2107
            $attachmentPreview.addClass('hidden');
2108
        }
2109
2110
        /**
2111
         * checks if there is an attachment
2112
         *
2113
         * @name   AttachmentViewer.hasAttachment
2114
         * @function
2115
         */
2116
        me.hasAttachment = function()
2117
        {
2118
            return ($attachmentLink.prop('href') !== '')
2119
        }
2120
2121
        /**
2122
         * return the attachment
2123
         *
2124
         * @name   AttachmentViewer.getAttachment
2125
         * @function
2126
         * @returns {array}
2127
         */
2128
        me.getAttachment = function()
2129
        {
2130
            return [
2131
                $attachmentLink.prop('href'),
2132
                $attachmentLink.prop('download')
2133
            ];
2134
        }
2135
2136
        /**
2137
         * moves the attachment link to another element
2138
         *
2139
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2140
         *
2141
         * @name   AttachmentViewer.setClonedAttachment
2142
         * @function
2143
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2144
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2145
         */
2146
        me.moveAttachmentTo = function($element, label)
2147
        {
2148
            // move elemement to new place
2149
            $attachmentLink.appendTo($element);
2150
2151
            // update text
2152
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2153
        }
2154
2155
        /**
2156
         * initiate
2157
         *
2158
         * preloads jQuery elements
2159
         *
2160
         * @name   AttachmentViewer.init
2161
         * @function
2162
         */
2163
        me.init = function()
2164
        {
2165
            $attachment = $('#attachment');
2166
            $attachmentLink = $('#attachment a');
2167
            $attachmentPreview = $('#attachmentPreview');
2168
        }
2169
2170
        return me;
2171
    })(window, document);
2172
2173
    /**
2174
     * (view) Shows discussion thread and handles replies
2175
     *
2176
     * @param  {object} window
2177
     * @param  {object} document
2178
     * @class
2179
     */
2180
    var DiscussionViewer = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window 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...
Unused Code introduced by
The parameter document 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...
2181
        var me = {};
2182
2183
        var $commentTail,
2184
            $discussion,
2185
            $reply,
2186
            $replyMessage,
2187
            $replyNickname,
2188
            $replyStatus,
2189
            $commentContainer;
2190
2191
        var replyCommentId;
2192
2193
        /**
2194
         * initializes the templates
2195
         *
2196
         * @private
2197
         * @function
2198
         */
2199
        function initTemplates()
2200
        {
2201
            $reply = Model.getTemplate('reply');
2202
            $replyMessage = $reply.find('#replymessage');
2203
            $replyNickname = $reply.find('#nickname');
2204
            $replyStatus = $reply.find('#replystatus');
2205
2206
            // cache jQuery elements
2207
            $commentTail = Model.getTemplate('commenttail');
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
Unused Code introduced by
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...
Unused Code introduced by
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...
Unused Code introduced by
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
         * open the comment entry when clicking the "Reply" button of a comment
2245
         *
2246
         * @private
2247
         * @function
2248
         * @param  {Event} event
2249
         */
2250
        function openReply(event)
2251
        {
2252
            var $source = $(event.target);
2253
2254
            // clear input
2255
            $replyMessage.val('');
2256
            $replyNickname.val('');
2257
2258
            // get comment id from source element
2259
            replyCommentId = $source.parent().prop('id').split('_')[1];
2260
2261
            // move to correct position
2262
            $source.after($reply);
2263
2264
            // show
2265
            $reply.removeClass('hidden');
2266
            $replyMessage.focus();
2267
2268
            event.preventDefault();
2269
        }
2270
2271
        /**
2272
         * adds another comment
2273
         *
2274
         * @name   DiscussionViewer.addComment
2275
         * @function
2276
         * @param {object} comment
2277
         * @param {string} commentText
2278
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2279
         */
2280
        me.addComment = function(comment, commentText, nickname, $place)
2281
        {
2282
            if (typeof $place === 'undefined') {
2283
                // starting point (default value/fallback)
2284
                $place = $commentContainer;
2285
2286
                // if parent comment exists
2287
                var $parentComment = $('#comment_' + comment.parentid);
2288
                if ($parentComment.length) {
2289
                    // use parent as position for noew comment, so it shifted
2290
                    // to the right
2291
                    $place = $parentComment;
2292
                }
2293
            }
2294
            if (commentText === '') {
2295
                commentText = 'comment decryption failed';
2296
            }
2297
2298
            // create new comment based on template
2299
            var $commentEntry = Model.getTemplate('comment');
2300
            $commentEntry.prop('id', 'comment_' + comment.id);
2301
            var $commentEntryData = $commentEntry.find('div.commentdata');
2302
2303
            // set & parse text
2304
            Helper.setElementText($commentEntryData, commentText);
2305
            Helper.urls2links($commentEntryData);
2306
2307
            // set nickname
2308
            if (nickname.length > 0){
2309
                $commentEntry.find('span.nickname').text(nickname);
2310
            } else {
2311
                $commentEntry.find('span.nickname').html('<i>' + I18n._('Anonymous') + '</i>');
2312
            }
2313
2314
            // set date
2315
            $commentEntry.find('span.commentdate')
2316
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2317
                      .attr('title', 'CommentID: ' + comment.id);
2318
2319
            // if an avatar is available, display it
2320
            if (comment.meta.vizhash) {
2321
                $commentEntry.find('span.nickname')
2322
                          .before(
2323
                            '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
2324
                            I18n._('Avatar generated from IP address') + '" /> '
2325
                          );
2326
            }
2327
2328
            // finally append comment
2329
            $place.append($commentEntry);
2330
        }
2331
2332
        /**
2333
         * finishes the discussion area after last comment
2334
         *
2335
         * @name   DiscussionViewer.finishDiscussion
2336
         * @function
2337
         */
2338
        me.finishDiscussion = function()
2339
        {
2340
            // add 'add new comment' area
2341
            $commentContainer.append($commentTail);
2342
2343
            // show discussions
2344
            $discussion.removeClass('hidden');
2345
        }
2346
2347
        /**
2348
         * shows the discussion area
2349
         *
2350
         * @name   DiscussionViewer.showDiscussion
2351
         * @function
2352
         */
2353
        me.showDiscussion = function()
2354
        {
2355
            $discussion.removeClass('hidden');
2356
        }
2357
2358
        /**
2359
         * removes the old discussion and prepares everything for creating a new
2360
         * one.
2361
         *
2362
         * @name   DiscussionViewer.prepareNewDisucssion
2363
         * @function
2364
         */
2365
        me.prepareNewDisucssion = function()
2366
        {
2367
            $commentContainer.html('');
2368
            $discussion.addClass('hidden');
2369
2370
            // (re-)init templates
2371
            initTemplates();
2372
        }
2373
2374
        /**
2375
         * returns the user put into the reply form
2376
         *
2377
         * @name   DiscussionViewer.getReplyData
2378
         * @function
2379
         * @return {array}
2380
         */
2381
        me.getReplyData = function()
2382
        {
2383
            return [
2384
                $replyMessage.val(),
2385
                $replyNickname.val()
2386
            ];
2387
        }
2388
2389
        /**
2390
         * highlights a specific comment and scrolls to it if necessary
2391
         *
2392
         * @name   DiscussionViewer.highlightComment
2393
         * @function
2394
         * @param {string} commentId
2395
         * @param {bool} fadeOut - whether to fade out the comment
2396
         */
2397
        me.highlightComment = function(commentId, fadeOut)
2398
        {
2399
            var $comment = $('#comment_' + commentId);
2400
            // in case comment does not exist, cancel
2401
            if ($comment.length === 0) {
2402
                return;
2403
            }
2404
2405
            var highlightComment = function () {
2406
                $comment.addClass('highlight');
2407
                if (fadeOut === true) {
2408
                    setTimeout(function () {
2409
                        $comment.removeClass('highlight');
2410
                    }, 300);
2411
                }
2412
            }
2413
2414
            if (UiHelper.isVisible($comment)) {
2415
                return highlightComment();
2416
            }
2417
2418
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
0 ignored issues
show
Best Practice introduced by
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...
2419
        }
2420
2421
        /**
2422
         * returns the id of the parent comment the user is replying to
2423
         *
2424
         * @name   DiscussionViewer.getReplyCommentId
2425
         * @function
2426
         * @return {int|undefined}
2427
         */
2428
        me.getReplyCommentId = function()
2429
        {
2430
            return replyCommentId;
2431
        }
2432
2433
        /**
2434
         * initiate
2435
         *
2436
         * preloads jQuery elements
2437
         *
2438
         * @name   DiscussionViewer.init
2439
         * @function
2440
         */
2441
        me.init = function()
2442
        {
2443
            // bind events to templates (so they are later cloned)
2444
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2445
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2446
2447
            $commentContainer = $('#commentcontainer');
2448
            $discussion = $('#discussion');
2449
        }
2450
2451
        return me;
2452
    })(window, document);
2453
2454
    /**
2455
     * Manage top (navigation) bar
2456
     *
2457
     * @param {object} window
2458
     * @param {object} document
2459
     * @class
2460
     */
2461
    var TopNav = (function (window, document) {
2462
        var me = {};
2463
2464
        var createButtonsDisplayed = false;
2465
        var viewButtonsDisplayed = false;
2466
2467
        var $attach,
2468
            $burnAfterReading,
2469
            $burnAfterReadingOption,
2470
            $cloneButton,
2471
            $customAttachment,
2472
            $expiration,
2473
            $fileRemoveButton,
2474
            $fileWrap,
2475
            $formatter,
2476
            $newButton,
2477
            $openDiscussion,
2478
            $openDiscussionOption,
2479
            $password,
2480
            $passwordInput,
2481
            $rawTextButton,
2482
            $sendButton;
2483
2484
        var pasteExpiration = '1week';
2485
2486
        /**
2487
         * set the expiration on bootstrap templates in dropdown
2488
         *
2489
         * @name   TopNav.updateExpiration
2490
         * @function
2491
         * @param  {Event} event
2492
         */
2493
        function updateExpiration(event)
2494
        {
2495
            // get selected option
2496
            var target = $(event.target);
2497
2498
            // update dropdown display and save new expiration time
2499
            $('#pasteExpirationDisplay').text(target.text());
2500
            pasteExpiration = target.data('expiration');
2501
2502
            event.preventDefault();
2503
        }
2504
2505
        /**
2506
         * set the format on bootstrap templates in dropdown
2507
         *
2508
         * @name   TopNav.updateFormat
2509
         * @function
2510
         * @param  {Event} event
2511
         */
2512
        function updateFormat(event)
2513
        {
2514
            // get selected option
2515
            var $target = $(event.target);
2516
2517
            // update dropdown display and save new format
2518
            var newFormat = $target.data('format');
2519
            $('#pasteFormatterDisplay').text($target.text());
2520
            PasteViewer.setFormat(newFormat);
2521
2522
            // update preview
2523
            if (Editor.isPreview()) {
2524
                PasteViewer.run();
2525
            }
2526
2527
            event.preventDefault();
2528
        }
2529
2530
        /**
2531
         * when "burn after reading" is checked, disable discussion
2532
         *
2533
         * @name   TopNav.changeBurnAfterReading
2534
         * @function
2535
         */
2536
        function changeBurnAfterReading()
2537
        {
2538
            if ($burnAfterReading.is(':checked')) {
2539
                $openDiscussionOption.addClass('buttondisabled');
2540
                $openDiscussion.prop('checked', false);
2541
2542
                // if button is actually disabled, force-enable it and uncheck other button
2543
                $burnAfterReadingOption.removeClass('buttondisabled');
2544
            } else {
2545
                $openDiscussionOption.removeClass('buttondisabled');
2546
            }
2547
        }
2548
2549
        /**
2550
         * when discussion is checked, disable "burn after reading"
2551
         *
2552
         * @name   TopNav.changeOpenDiscussion
2553
         * @function
2554
         */
2555
        function changeOpenDiscussion()
2556
        {
2557
            if ($openDiscussion.is(':checked')) {
2558
                $burnAfterReadingOption.addClass('buttondisabled');
2559
                $burnAfterReading.prop('checked', false);
2560
2561
                // if button is actually disabled, force-enable it and uncheck other button
2562
                $openDiscussionOption.removeClass('buttondisabled');
2563
            } else {
2564
                $burnAfterReadingOption.removeClass('buttondisabled');
2565
            }
2566
        }
2567
2568
        /**
2569
         * return raw text
2570
         *
2571
         * @name   TopNav.rawText
2572
         * @function
2573
         * @param  {Event} event
2574
         */
2575
        function rawText(event)
0 ignored issues
show
Unused Code introduced by
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...
2576
        {
2577
            TopNav.hideAllButtons();
2578
            Alert.showLoading('Showing raw text…', 0, 'time');
2579
            var paste = PasteViewer.getText();
2580
2581
            // push a new state to allow back navigation with browser back button
2582
            history.pushState(
2583
                {type: 'raw'},
2584
                document.title,
2585
                // recreate paste URL
2586
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2587
                Model.getPasteKey()
2588
            );
2589
2590
            // we use text/html instead of text/plain to avoid a bug when
2591
            // reloading the raw text view (it reverts to type text/html)
2592
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2593
            var newDoc = document.open('text/html', 'replace');
2594
            newDoc.write('<!DOCTYPE html><html><head>');
2595
            for (var i = 0; i < $head.length; i++) {
2596
                newDoc.write($head[i].outerHTML);
2597
            }
2598
            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
2599
            newDoc.close();
2600
        }
2601
2602
        /**
2603
         * saves the language in a cookie and reloads the page
2604
         *
2605
         * @name   TopNav.setLanguage
2606
         * @function
2607
         * @param  {Event} event
2608
         */
2609
        function setLanguage(event)
2610
        {
2611
            document.cookie = 'lang=' + $(event.target).data('lang');
2612
            UiHelper.reloadHome();
2613
        }
2614
2615
        /**
2616
         * hides all messages and creates a new paste
2617
         *
2618
         * @private
2619
         * @function
2620
         * @param  {Event} event
2621
         */
2622
        function clickNewPaste(event)
0 ignored issues
show
Unused Code introduced by
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...
2623
        {
2624
            Controller.hideStatusMessages();
2625
            Controller.newPaste();
2626
        }
2627
2628
        /**
2629
         * removes the existing attachment
2630
         *
2631
         * @private
2632
         * @function
2633
         * @param  {Event} event
2634
         */
2635
        function removeAttachment(event)
2636
        {
2637
            // if custom attachment is used, remove it first
2638
            if (!$customAttachment.hasClass('hidden')) {
2639
                AttachmentViewer.removeAttachment();
2640
                $customAttachment.addClass('hidden');
2641
                $fileWrap.removeClass('hidden');
2642
            }
2643
2644
            // our up-to-date jQuery can handle it :)
2645
            $fileWrap.find('input').val('');
2646
2647
            // pevent '#' from appearing in the URL
2648
            event.preventDefault();
2649
        }
2650
2651
        /**
2652
         * Loads the default options for creating a paste.
2653
         *
2654
         * @name   TopNav.loadDefaults
2655
         * @function
2656
         */
2657
        me.loadDefaults = function()
2658
        {
2659
            // @TODO
2660
        }
2661
2662
        /**
2663
         * Shows all elements belonging to viwing an existing pastes
2664
         *
2665
         * @name   TopNav.showViewButtons
2666
         * @function
2667
         */
2668
        me.showViewButtons = function()
2669
        {
2670
            if (viewButtonsDisplayed) {
2671
                console.log('showViewButtons: view buttons are already displayed');
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
2672
                return;
2673
            }
2674
2675
            $newButton.removeClass('hidden');
2676
            $cloneButton.removeClass('hidden');
2677
            $rawTextButton.removeClass('hidden');
2678
2679
            viewButtonsDisplayed = true;
2680
        }
2681
2682
        /**
2683
         * Hides all elements belonging to existing pastes
2684
         *
2685
         * @name   TopNav.hideViewButtons
2686
         * @function
2687
         */
2688
        me.hideViewButtons = function()
2689
        {
2690
            if (!viewButtonsDisplayed) {
2691
                console.log('hideViewButtons: view buttons are already hidden');
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
2692
                return;
2693
            }
2694
2695
            $newButton.addClass('hidden');
2696
            $cloneButton.addClass('hidden');
2697
            $rawTextButton.addClass('hidden');
2698
2699
            viewButtonsDisplayed = false;
2700
        }
2701
2702
        /**
2703
         * Hides all elements belonging to existing pastes
2704
         *
2705
         * @name   TopNav.hideAllButtons
2706
         * @function
2707
         */
2708
        me.hideAllButtons = function()
2709
        {
2710
            me.hideViewButtons();
2711
            me.hideCreateButtons();
2712
        }
2713
2714
        /**
2715
         * shows all elements needed when creating a new paste
2716
         *
2717
         * @name   TopNav.showCreateButtons
2718
         * @function
2719
         */
2720
        me.showCreateButtons = function()
2721
        {
2722
            if (createButtonsDisplayed) {
2723
                console.log('showCreateButtons: create buttons are already displayed');
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
2724
                return;
2725
            }
2726
2727
            $sendButton.removeClass('hidden');
2728
            $expiration.removeClass('hidden');
2729
            $formatter.removeClass('hidden');
2730
            $burnAfterReadingOption.removeClass('hidden');
2731
            $openDiscussionOption.removeClass('hidden');
2732
            $newButton.removeClass('hidden');
2733
            $password.removeClass('hidden');
2734
            $attach.removeClass('hidden');
2735
2736
            createButtonsDisplayed = true;
2737
        }
2738
2739
        /**
2740
         * shows all elements needed when creating a new paste
2741
         *
2742
         * @name   TopNav.hideCreateButtons
2743
         * @function
2744
         */
2745
        me.hideCreateButtons = function()
2746
        {
2747
            if (!createButtonsDisplayed) {
2748
                console.log('hideCreateButtons: create buttons are already hidden');
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
2749
                return;
2750
            }
2751
2752
            $newButton.addClass('hidden');
2753
            $sendButton.addClass('hidden');
2754
            $expiration.addClass('hidden');
2755
            $formatter.addClass('hidden');
2756
            $burnAfterReadingOption.addClass('hidden');
2757
            $openDiscussionOption.addClass('hidden');
2758
            $password.addClass('hidden');
2759
            $attach.addClass('hidden');
2760
2761
            createButtonsDisplayed = false;
2762
        }
2763
2764
        /**
2765
         * only shows the "new paste" button
2766
         *
2767
         * @name   TopNav.showNewPasteButton
2768
         * @function
2769
         */
2770
        me.showNewPasteButton = function()
2771
        {
2772
            $newButton.removeClass('hidden');
2773
        }
2774
2775
        /**
2776
         * only hides the clone button
2777
         *
2778
         * @name   TopNav.hideCloneButton
2779
         * @function
2780
         */
2781
        me.hideCloneButton = function()
2782
        {
2783
            $cloneButton.addClass('hidden');
2784
        }
2785
2786
        /**
2787
         * only hides the raw text button
2788
         *
2789
         * @name   TopNav.hideRawButton
2790
         * @function
2791
         */
2792
        me.hideRawButton = function()
2793
        {
2794
            $rawTextButton.addClass('hidden');
2795
        }
2796
2797
        /**
2798
         * hides the file selector in attachment
2799
         *
2800
         * @name   TopNav.hideFileSelector
2801
         * @function
2802
         */
2803
        me.hideFileSelector = function()
2804
        {
2805
            $fileWrap.addClass('hidden');
2806
        }
2807
2808
2809
        /**
2810
         * shows the custom attachment
2811
         *
2812
         * @name   TopNav.showCustomAttachment
2813
         * @function
2814
         */
2815
        me.showCustomAttachment = function()
2816
        {
2817
            $customAttachment.removeClass('hidden');
2818
        }
2819
2820
        /**
2821
         * collapses the navigation bar if nedded
2822
         *
2823
         * @name   TopNav.collapseBar
2824
         * @function
2825
         */
2826
        me.collapseBar = function()
2827
        {
2828
            var $bar = $('.navbar-toggle');
2829
2830
            // check if bar is expanded
2831
            if ($bar.hasClass('collapse in')) {
2832
                // if so, toggle it
2833
                $bar.click();
2834
            }
2835
        }
2836
2837
        /**
2838
         * returns the currently set expiration time
2839
         *
2840
         * @name   TopNav.getExpiration
2841
         * @function
2842
         * @return {int}
2843
         */
2844
        me.getExpiration = function()
2845
        {
2846
            return pasteExpiration;
2847
        }
2848
2849
        /**
2850
         * returns the currently selected file(s)
2851
         *
2852
         * @name   TopNav.getFileList
2853
         * @function
2854
         * @return {FileList|null}
2855
         */
2856
        me.getFileList = function()
2857
        {
2858
            var $file = $('#file');
2859
2860
            // if no file given, return null
2861
            if (!$file.length || !$file[0].files.length) {
2862
                return null;
2863
            }
2864
            // @TODO is this really necessary
2865
            if (!($file[0].files && $file[0].files[0])) {
2866
                return null;
2867
            }
2868
2869
            return $file[0].files;
2870
        }
2871
2872
        /**
2873
         * returns the state of the burn after reading checkbox
2874
         *
2875
         * @name   TopNav.getExpiration
2876
         * @function
2877
         * @return {bool}
2878
         */
2879
        me.getBurnAfterReading = function()
2880
        {
2881
            return $burnAfterReading.is(':checked');
2882
        }
2883
2884
        /**
2885
         * returns the state of the discussion checkbox
2886
         *
2887
         * @name   TopNav.getOpenDiscussion
2888
         * @function
2889
         * @return {bool}
2890
         */
2891
        me.getOpenDiscussion = function()
2892
        {
2893
            return $openDiscussion.is(':checked');
2894
        }
2895
2896
        /**
2897
         * returns the entered password
2898
         *
2899
         * @name   TopNav.getPassword
2900
         * @function
2901
         * @return {string}
2902
         */
2903
        me.getPassword = function()
2904
        {
2905
            return $passwordInput.val();
2906
        }
2907
2908
        /**
2909
         * returns the element where custom attachments can be placed
2910
         *
2911
         * Used by AttachmentViewer when an attachment is cloned here.
2912
         *
2913
         * @name   TopNav.getCustomAttachment
2914
         * @function
2915
         * @return {jQuery}
2916
         */
2917
        me.getCustomAttachment = function()
2918
        {
2919
            return $customAttachment;
2920
        }
2921
2922
        /**
2923
         * init navigation manager
2924
         *
2925
         * preloads jQuery elements
2926
         *
2927
         * @name   TopNav.init
2928
         * @function
2929
         */
2930
        me.init = function()
2931
        {
2932
            $attach = $('#attach');
2933
            $burnAfterReading = $('#burnafterreading');
2934
            $burnAfterReadingOption = $('#burnafterreadingoption');
2935
            $cloneButton = $('#clonebutton');
2936
            $customAttachment = $('#customattachment');
2937
            $expiration = $('#expiration');
2938
            $fileRemoveButton = $('#fileremovebutton');
2939
            $fileWrap = $('#filewrap');
2940
            $formatter = $('#formatter');
2941
            $newButton = $('#newbutton');
2942
            $openDiscussion = $('#opendiscussion');
2943
            $openDiscussionOption = $('#opendiscussionoption');
2944
            $password = $('#password');
2945
            $passwordInput = $('#passwordinput');
2946
            $rawTextButton = $('#rawtextbutton');
2947
            $sendButton = $('#sendbutton');
2948
2949
            // bootstrap template drop down
2950
            $('#language ul.dropdown-menu li a').click(setLanguage);
2951
            // page template drop down
2952
            $('#language select option').click(setLanguage);
2953
2954
            // bind events
2955
            $burnAfterReading.change(changeBurnAfterReading);
2956
            $openDiscussionOption.change(changeOpenDiscussion);
2957
            $newButton.click(clickNewPaste);
2958
            $sendButton.click(PasteEncrypter.submitPaste);
2959
            $cloneButton.click(Controller.clonePaste);
2960
            $rawTextButton.click(rawText);
2961
            $fileRemoveButton.click(removeAttachment);
2962
2963
            // bootstrap template drop downs
2964
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2965
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2966
2967
            // initiate default state of checkboxes
2968
            changeBurnAfterReading();
2969
            changeOpenDiscussion();
2970
2971
            // get default value from template or fall back to set value
2972
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2973
2974
            me.loadDefaults();
2975
        }
2976
2977
        return me;
2978
    })(window, document);
2979
2980
    /**
2981
     * Responsible for AJAX requests, transparently handles encryption…
2982
     *
2983
     * @class
2984
     */
2985
    var Uploader = (function () {
2986
        var me = {};
2987
2988
        var successFunc = null,
2989
            failureFunc = null,
2990
            url,
2991
            data,
2992
            symmetricKey,
2993
            password;
2994
2995
        /**
2996
         * public variable ('constant') for errors to prevent magic numbers
2997
         *
2998
         * @readonly
2999
         * @enum   {Object}
3000
         */
3001
        me.error = {
3002
            okay: 0,
3003
            custom: 1,
3004
            unknown: 2,
3005
            serverError: 3
3006
        };
3007
3008
        /**
3009
         * ajaxHeaders to send in AJAX requests
3010
         *
3011
         * @private
3012
         * @readonly
3013
         * @enum   {Object}
3014
         */
3015
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
3016
3017
        /**
3018
         * called after successful upload
3019
         *
3020
         * @private
3021
         * @function
3022
         * @throws {string}
3023
         */
3024
        function checkCryptParameters()
3025
        {
3026
            // workaround for this nasty 'bug' in ECMAScript
3027
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3028
            var typeOfKey = typeof symmetricKey;
3029
            if (symmetricKey === null) {
3030
                typeOfKey = 'null';
3031
            }
3032
3033
            // in case of missing preparation, throw error
3034
            switch (typeOfKey) {
3035
                case 'string':
3036
                    // already set, all right
3037
                    return;
3038
                case 'null':
3039
                    // needs to be generated auto-generate
3040
                    symmetricKey = CryptTool.getSymmetricKey();
3041
                    break;
3042
                default:
3043
                    console.error('current invalid symmetricKey:', symmetricKey);
3044
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3045
            }
3046
            // password is optional
3047
        }
3048
3049
        /**
3050
         * called after successful upload
3051
         *
3052
         * @private
3053
         * @function
3054
         * @param {int} status
3055
         * @param {int} data - optional
0 ignored issues
show
Documentation introduced by
The parameter data does not exist. Did you maybe forget to remove this comment?
Loading history...
3056
         */
3057
        function success(status, result)
3058
        {
3059
            // add useful data to result
3060
            result.encryptionKey = symmetricKey;
3061
            result.requestData = data;
3062
3063
            if (successFunc !== null) {
3064
                successFunc(status, result);
3065
            }
3066
        }
3067
3068
        /**
3069
         * called after a upload failure
3070
         *
3071
         * @private
3072
         * @function
3073
         * @param {int} status - internal code
3074
         * @param {int} data - original error code
0 ignored issues
show
Documentation introduced by
The parameter data does not exist. Did you maybe forget to remove this comment?
Loading history...
3075
         */
3076
        function fail(status, result)
3077
        {
3078
            if (failureFunc !== null) {
3079
                failureFunc(status, result);
3080
            }
3081
        }
3082
3083
        /**
3084
         * actually uploads the data
3085
         *
3086
         * @name   Uploader.run
3087
         * @function
3088
         */
3089
        me.run = function()
3090
        {
3091
            $.ajax({
3092
                type: 'POST',
3093
                url: url,
3094
                data: data,
3095
                dataType: 'json',
3096
                headers: ajaxHeaders,
3097
                success: function(result) {
3098
                    if (result.status === 0) {
3099
                        success(0, result);
3100
                    } else if (result.status === 1) {
3101
                        fail(1, result);
3102
                    } else {
3103
                        fail(2, result);
3104
                    }
3105
                }
3106
            })
3107
            .fail(function(jqXHR, textStatus, errorThrown) {
3108
                console.error(textStatus, errorThrown);
3109
                fail(3, jqXHR);
3110
            });
3111
        }
3112
3113
        /**
3114
         * set success function
3115
         *
3116
         * @name   Uploader.setSuccess
3117
         * @function
3118
         * @param {function} func
0 ignored issues
show
Documentation introduced by
The parameter func does not exist. Did you maybe forget to remove this comment?
Loading history...
3119
         */
3120
        me.setUrl = function(newUrl)
3121
        {
3122
            url = newUrl;
3123
        }
3124
3125
        /**
3126
         * sets the password to use (first value) and optionally also the
3127
         * encryption key (not recommend, it is automatically generated).
3128
         *
3129
         * Note: Call this after prepare() as prepare() resets these values.
3130
         *
3131
         * @name   Uploader.setCryptValues
3132
         * @function
3133
         * @param {string} newPassword
3134
         * @param {string} newKey       - optional
3135
         */
3136
        me.setCryptParameters = function(newPassword, newKey)
3137
        {
3138
            password = newPassword;
3139
3140
            if (typeof newKey !== 'undefined') {
3141
                symmetricKey = newKey;
3142
            }
3143
        }
3144
3145
        /**
3146
         * set success function
3147
         *
3148
         * @name   Uploader.setSuccess
3149
         * @function
3150
         * @param {function} func
3151
         */
3152
        me.setSuccess = function(func)
3153
        {
3154
            successFunc = func;
3155
        }
3156
3157
        /**
3158
         * set failure function
3159
         *
3160
         * @name   Uploader.setSuccess
3161
         * @function
3162
         * @param {function} func
3163
         */
3164
        me.setFailure = function(func)
3165
        {
3166
            failureFunc = func;
3167
        }
3168
3169
        /**
3170
         * prepares a new upload
3171
         *
3172
         * Call this when doing a new upload to reset any data from potential
3173
         * previous uploads. Must be called before any other method of this
3174
         * module.
3175
         *
3176
         * @name   Uploader.prepare
3177
         * @function
3178
         * @return {object}
3179
         */
3180
        me.prepare = function()
3181
        {
3182
            // entropy should already be checked!
3183
3184
            // reset password
3185
            password = '';
3186
3187
            // reset key, so it a new one is generated when it is used
3188
            symmetricKey = null;
3189
3190
            // reset data
3191
            successFunc = null;
3192
            failureFunc = null;
3193
            url = Helper.baseUri();
3194
            data = {};
3195
        }
3196
3197
        /**
3198
         * encrypts and sets the data
3199
         *
3200
         * @name   Uploader.setData
3201
         * @function
3202
         * @param {string} index
3203
         * @param {mixed} element
3204
         */
3205
        me.setData = function(index, element)
3206
        {
3207
            checkCryptParameters();
3208
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3209
        }
3210
3211
        /**
3212
         * set the additional metadata to send unencrypted
3213
         *
3214
         * @name   Uploader.setUnencryptedData
3215
         * @function
3216
         * @param {string} index
3217
         * @param {mixed} element
3218
         */
3219
        me.setUnencryptedData = function(index, element)
3220
        {
3221
            data[index] = element;
3222
        }
3223
3224
        /**
3225
         * set the additional metadata to send unencrypted passed at once
3226
         *
3227
         * @name   Uploader.setUnencryptedData
3228
         * @function
3229
         * @param {object} newData
3230
         */
3231
        me.setUnencryptedBulkData = function(newData)
3232
        {
3233
            $.extend(data, newData);
3234
        }
3235
3236
        /**
3237
         * Helper, which parses shows a general error message based on the result of the Uploader
3238
         *
3239
         * @name    Uploader.parseUploadError
3240
         * @function
3241
         * @param {int} status
3242
         * @param {object} data
3243
         * @param {string} doThisThing - a human description of the action, which was tried
3244
         * @return {array}
3245
         */
3246
        me.parseUploadError = function(status, data, doThisThing) {
3247
            var errorArray = ['Error while parsing error message.'];
0 ignored issues
show
Unused Code introduced by
The assignment to variable errorArray seems to be never used. Consider removing it.
Loading history...
3248
3249
            switch (status) {
3250
                case Uploader.error['custom']:
0 ignored issues
show
Bug introduced by
The variable Uploader seems to be never declared. If this is a global, consider adding a /** global: Uploader */ 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...
3251
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3252
                    break;
3253
                case Uploader.error['unknown']:
3254
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3255
                    break;
3256
                case Uploader.error['serverError']:
3257
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];                        break;
3258
                default:
3259
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3260
                    break;
3261
            }
3262
3263
            return errorArray;
3264
        }
3265
3266
        /**
3267
         * init Uploader
3268
         *
3269
         * @name   Uploader.init
3270
         * @function
3271
         */
3272
        me.init = function()
3273
        {
3274
            // nothing yet
3275
        }
3276
3277
        return me;
3278
    })();
3279
3280
    /**
3281
     * (controller) Responsible for encrypting paste and sending it to server.
3282
     *
3283
     * Does upload, encryption is done transparently by Uploader.
3284
     *
3285
     * @name state
3286
     * @class
3287
     */
3288
    var PasteEncrypter = (function () {
3289
        var me = {};
3290
3291
        var requirementsChecked = false;
3292
3293
        /**
3294
         * checks whether there is a suitable amount of entrophy
3295
         *
3296
         * @private
3297
         * @function
3298
         * @param {function} retryCallback - the callback to execute to retry the upload
3299
         * @return {bool}
3300
         */
3301
        function checkRequirements(retryCallback) {
3302
            // skip double requirement checks
3303
            if (requirementsChecked === true) {
3304
                return true;
3305
            }
3306
3307
            if (!CryptTool.isEntropyReady()) {
3308
                // display a message and wait
3309
                Alert.showStatus('Please move your mouse for more entropy…');
3310
3311
                CryptTool.addEntropySeedListener(retryCallback);
3312
                return false;
3313
            }
3314
3315
            requirementsChecked = true;
3316
3317
            return true;
3318
        }
3319
3320
        /**
3321
         * called after successful paste upload
3322
         *
3323
         * @private
3324
         * @function
3325
         * @param {int} status
3326
         * @param {object} data
3327
         */
3328
        function showCreatedPaste(status, data) {
3329
            Alert.hideLoading();
3330
3331
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3332
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3333
3334
            Alert.hideMessages();
3335
3336
            // show notification
3337
            PasteStatus.createPasteNotification(url, deleteUrl)
3338
3339
            // show new URL in browser bar
3340
            history.pushState({type: 'newpaste'}, document.title, url);
3341
3342
            TopNav.showViewButtons();
3343
            TopNav.hideRawButton();
3344
            Editor.hide();
3345
3346
            // parse and show text
3347
            // (preparation already done in me.submitPaste())
3348
            PasteViewer.run();
3349
        }
3350
3351
        /**
3352
         * called after successful comment upload
3353
         *
3354
         * @private
3355
         * @function
3356
         * @param {int} status
3357
         * @param {object} data
3358
         */
3359
        function showUploadedComment(status, data) {
3360
            // show success message
3361
            // Alert.showStatus('Comment posted.');
3362
3363
            // reload paste
3364
            Controller.refreshPaste(function () {
3365
                // highlight sent comment
3366
                DiscussionViewer.highlightComment(data.id, true);
3367
                // reset error handler
3368
                Alert.setCustomHandler(null);
3369
            });
3370
        }
3371
3372
        /**
3373
         * adds attachments to the Uploader
3374
         *
3375
         * @private
3376
         * @function
3377
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3378
         * @param {function} callback - excuted when action is successful
3379
         */
3380
        function encryptAttachments(file, callback) {
3381
            if (typeof file !== 'undefined' && file !== null) {
3382
                // check file reader requirements for upload
3383
                if (typeof FileReader === 'undefined') {
3384
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3385
                    // cancels process as it does not execute callback
3386
                    return;
3387
                }
3388
3389
                var reader = new FileReader();
3390
3391
                // closure to capture the file information
3392
                reader.onload = function(event) {
3393
                    Uploader.setData('attachment', event.target.result);
3394
                    Uploader.setData('attachmentname', file.name);
3395
3396
                    // run callback
3397
                    return callback();
3398
                }
3399
3400
                // actually read first file
3401
                reader.readAsDataURL(file);
0 ignored issues
show
Best Practice introduced by
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...
3402
            } else if (AttachmentViewer.hasAttachment()) {
3403
                // fall back to cloned part
3404
                var attachment = AttachmentViewer.getAttachment();
3405
3406
                Uploader.setData('attachment', attachment[0]);
3407
                Uploader.setData('attachmentname', attachment[1]);
3408
                return callback();
3409
            } else {
3410
                // if there are no attachments, this is of course still successful
3411
                return callback();
3412
            }
3413
        }
3414
3415
        /**
3416
         * send a reply in a discussion
3417
         *
3418
         * @name   PasteEncrypter.sendComment
3419
         * @function
3420
         */
3421
        me.sendComment = function()
3422
        {
3423
            Alert.hideMessages();
3424
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3425
3426
            // UI loading state
3427
            TopNav.hideAllButtons();
3428
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3429
3430
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3431
            var replyData = DiscussionViewer.getReplyData(),
3432
                plainText = replyData[0],
3433
                nickname = replyData[1],
3434
                parentid = DiscussionViewer.getReplyCommentId();
3435
3436
            // do not send if there is no data
3437
            if (plainText.length === 0) {
3438
                // revert loading status…
3439
                Alert.hideLoading();
3440
                Alert.setCustomHandler(null);
3441
                TopNav.showViewButtons();
3442
                return;
3443
            }
3444
3445
            // check entropy
3446
            if (!checkRequirements(function () {
3447
                me.sendComment();
3448
            })) {
3449
                return; // to prevent multiple executions
3450
            }
3451
            Alert.showLoading(null, 10);
3452
3453
            // prepare Uploader
3454
            Uploader.prepare();
3455
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3456
3457
            // set success/fail functions
3458
            Uploader.setSuccess(showUploadedComment);
3459
            Uploader.setFailure(function (status, data) {
3460
                // revert loading status…
3461
                Alert.hideLoading();
3462
                TopNav.showViewButtons();
3463
3464
                // show error message
3465
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3466
3467
                // reset error handler
3468
                Alert.setCustomHandler(null);
3469
            });
3470
3471
            // fill it with unencrypted params
3472
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3473
            if (typeof parentid === 'undefined') {
3474
                // if parent id is not set, this is the top-most comment, so use
3475
                // paste id as parent @TODO is this really good?
3476
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3477
            } else {
3478
                Uploader.setUnencryptedData('parentid', parentid);
3479
            }
3480
3481
            // encrypt data
3482
            Uploader.setData('data', plainText);
3483
3484
            if (nickname.length > 0) {
3485
                Uploader.setData('nickname', nickname);
3486
            }
3487
3488
            Uploader.run();
3489
        }
3490
3491
        /**
3492
         * sends a new paste to server
3493
         *
3494
         * @name   PasteEncrypter.submitPaste
3495
         * @function
3496
         */
3497
        me.submitPaste = function()
3498
        {
3499
            // hide previous (error) messages
3500
            Controller.hideStatusMessages();
3501
3502
            // UI loading state
3503
            TopNav.hideAllButtons();
3504
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3505
            TopNav.collapseBar();
3506
3507
            // get data
3508
            var plainText = Editor.getText(),
3509
                format = PasteViewer.getFormat(),
3510
                files = TopNav.getFileList();
3511
3512
            // do not send if there is no data
3513
            if (plainText.length === 0 && files === null) {
3514
                // revert loading status…
3515
                Alert.hideLoading();
3516
                TopNav.showCreateButtons();
3517
                return;
3518
            }
3519
3520
            Alert.showLoading(null, 10);
3521
3522
            // check entropy
3523
            if (!checkRequirements(function () {
3524
                me.submitPaste();
3525
            })) {
3526
                return; // to prevent multiple executions
3527
            }
3528
3529
            // prepare Uploader
3530
            Uploader.prepare();
3531
            Uploader.setCryptParameters(TopNav.getPassword());
3532
3533
            // set success/fail functions
3534
            Uploader.setSuccess(showCreatedPaste);
3535
            Uploader.setFailure(function (status, data) {
3536
                // revert loading status…
3537
                Alert.hideLoading();
3538
                TopNav.showCreateButtons();
3539
3540
                // show error message
3541
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3542
            });
3543
3544
            // fill it with unencrypted submitted options
3545
            Uploader.setUnencryptedBulkData({
3546
                expire:           TopNav.getExpiration(),
3547
                formatter:        format,
3548
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3549
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3550
            });
3551
3552
            // prepare PasteViewer for later preview
3553
            PasteViewer.setText(plainText);
3554
            PasteViewer.setFormat(format);
3555
3556
            // encrypt cipher data
3557
            Uploader.setData('data', plainText);
3558
3559
            // encrypt attachments
3560
            encryptAttachments(
3561
                files === null ? null : files[0],
3562
                function () {
3563
                    // send data
3564
                    Uploader.run();
3565
                }
3566
            );
3567
        }
3568
3569
        /**
3570
         * initialize
3571
         *
3572
         * @name   PasteEncrypter.init
3573
         * @function
3574
         */
3575
        me.init = function()
3576
        {
3577
            // nothing yet
3578
        }
3579
3580
        return me;
3581
    })();
3582
3583
    /**
3584
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3585
     *
3586
     * Only decryption, no download.
3587
     *
3588
     * @name state
3589
     * @class
3590
     */
3591
    var PasteDecrypter = (function () {
3592
        var me = {};
3593
3594
        /**
3595
         * decrypt data or prompts for password in cvase of failure
3596
         *
3597
         * @private
3598
         * @function
3599
         * @param {string} key
3600
         * @param {string} password - optional, may be an empty string
3601
         * @param {string} cipherdata
3602
         * @throws {string}
3603
         * @return {false|string} - false, when unsuccessful or string (decrypted data)
3604
         */
3605
        function decryptOrPromptPassword(key, password, cipherdata)
3606
        {
3607
            // try decryption without password
3608
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3609
3610
            // if it fails, request password
3611
            if (plaindata.length === 0 && password.length === 0) {
3612
                // try to get cached password first
3613
                password = Prompt.getPassword();
3614
3615
                // if password is there, re-try
3616
                if (password.length !== 0) {
3617
                    // recursive
3618
                    // note: an infinite loop is prevented as the previous if
3619
                    // clause checks whether a password is already set and ignores
3620
                    // errors when a password has been passed
3621
                    return decryptOrPromptPassword.apply(arguments);
3622
                }
3623
3624
                // trigger password request
3625
                Prompt.requestPassword();
3626
                // the callback (via setPasswordCallback()) should have been set
3627
                // by a parent function
3628
                return false;
3629
            }
3630
3631
            // if all tries failed, we can only return an error
3632
            if (plaindata.length === 0) {
3633
                throw 'failed to decipher data';
3634
            }
3635
3636
            return plaindata;
3637
        }
3638
3639
        /**
3640
         * decrypt the actual paste text
3641
         *
3642
         * @private
3643
         * @function
3644
         * @param {object} paste - paste data in object form
3645
         * @param {string} key
3646
         * @param {string} password
3647
         * @param {bool} ignoreError - ignore decryption errors iof set to true
3648
         * @return {bool} - whether action was successful
3649
         * @throws {string}
3650
         */
3651
        function decryptPaste(paste, key, password, ignoreError)
3652
        {
3653
            var plaintext
3654
            if (ignoreError === true) {
3655
                plaintext = CryptTool.decipher(key, password, paste.data);
3656
            } else {
3657
                try {
3658
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3659
                } catch (err) {
3660
                    throw 'failed to decipher paste text: ' + err
3661
                }
3662
                if (plaintext === false) {
3663
                    return false;
3664
                }
3665
            }
3666
3667
            // on success show paste
3668
            PasteViewer.setFormat(paste.meta.formatter);
3669
            PasteViewer.setText(plaintext);
3670
            // trigger to show the text (attachment loaded afterwards)
3671
            PasteViewer.run();
3672
3673
            return true;
3674
        }
3675
3676
        /**
3677
         * decrypts any attachment
3678
         *
3679
         * @private
3680
         * @function
3681
         * @param {object} paste - paste data in object form
3682
         * @param {string} key
3683
         * @param {string} password
3684
         * @return {bool} - whether action was successful
3685
         * @throws {string}
3686
         */
3687
        function decryptAttachment(paste, key, password)
3688
        {
3689
            // decrypt attachment
3690
            try {
3691
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3692
            } catch (err) {
3693
                throw 'failed to decipher attachment: ' + err
3694
            }
3695
            if (attachment === false) {
3696
                return false;
3697
            }
3698
3699
            // decrypt attachment name
3700
            var attachmentName;
3701
            if (paste.attachmentname) {
3702
                try {
3703
                    var attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable attachmentName already seems to be declared on line 3700. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
3704
                } catch (err) {
3705
                    throw 'failed to decipher attachment name: ' + err
3706
                }
3707
                if (attachmentName === false) {
3708
                    return false;
3709
                }
3710
            }
3711
3712
            AttachmentViewer.setAttachment(attachment, attachmentName);
0 ignored issues
show
Bug introduced by
The variable attachmentName does not seem to be initialized in case paste.attachmentname on line 3701 is false. Are you sure the function setAttachment handles undefined variables?
Loading history...
3713
            AttachmentViewer.showAttachment();
3714
3715
            return true;
3716
        }
3717
3718
        /**
3719
         * decrypts all comments and shows them
3720
         *
3721
         * @private
3722
         * @function
3723
         * @param {object} paste - paste data in object form
3724
         * @param {string} key
3725
         * @param {string} password
3726
         * @return {bool} - whether action was successful
3727
         */
3728
        function decryptComments(paste, key, password)
3729
        {
3730
            // remove potentially previous discussion
3731
            DiscussionViewer.prepareNewDisucssion();
3732
3733
            // iterate over comments
3734
            for (var i = 0; i < paste.comments.length; ++i) {
3735
                var comment = paste.comments[i];
3736
3737
                DiscussionViewer.addComment(
3738
                    comment,
3739
                    CryptTool.decipher(key, password, comment.data),
3740
                    CryptTool.decipher(key, password, comment.meta.nickname)
3741
                );
3742
            }
3743
3744
            DiscussionViewer.finishDiscussion();
3745
            DiscussionViewer.showDiscussion();
3746
            return true;
3747
        }
3748
3749
        /**
3750
         * show decrypted text in the display area, including discussion (if open)
3751
         *
3752
         * @name   PasteDecrypter.run
3753
         * @function
3754
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3755
         */
3756
        me.run = function(paste)
3757
        {
3758
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3759
3760
            if (typeof paste === 'undefined') {
3761
                paste = $.parseJSON(Model.getCipherData());
3762
            }
3763
3764
            var key = Model.getPasteKey(),
3765
                password = Prompt.getPassword();
3766
3767
            if (PasteViewer.isPrettyPrinted()) {
3768
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3769
                return;
3770
            }
3771
3772
            // try to decrypt the paste
3773
            try {
3774
                Prompt.setPasswordCallback(function () {
3775
                    me.run(paste);
3776
                });
3777
3778
                // decrypt attachments
3779
                if (paste.attachment) {
3780
                    // try to decrypt paste and if it fails (because the password is
3781
                    // missing) return to let JS continue and wait for user
3782
                    if (!decryptAttachment(paste, key, password)) {
3783
                        return;
3784
                    }
3785
                }
3786
3787
                // Deliberately ignores non-critical errors as this decryption
3788
                // can also return an empty string and when this is done, the
3789
                // decryption routine cannot differenciate this to an error.
3790
                // As, however, the attachment could already be decrypted we
3791
                // can continue here without showing an error, but just an empty
3792
                // paste text.
3793
                decryptPaste(paste, key, password, true);
3794
            } catch(err) {
3795
                Alert.hideLoading();
3796
3797
                // log and show error
3798
                console.error(err);
3799
                Alert.showError('Could not decrypt data (Wrong key?)');
3800
3801
                // still go on to potentially show potentially partially decrypted data
3802
            }
3803
3804
            // shows the remaining time (until) deletion
3805
            PasteStatus.showRemainingTime(paste.meta);
3806
3807
            // if the discussion is opened on this paste, display it
3808
            if (paste.meta.opendiscussion) {
3809
                decryptComments(paste, key, password);
3810
            }
3811
3812
            Alert.hideLoading();
3813
            TopNav.showViewButtons();
3814
        }
3815
3816
        /**
3817
         * initialize
3818
         *
3819
         * @name   PasteDecrypter.init
3820
         * @function
3821
         */
3822
        me.init = function()
3823
        {
3824
            // nothing yet
3825
        }
3826
3827
        return me;
3828
    })();
3829
3830
    /**
3831
     * (controller) main PrivateBin logic
3832
     *
3833
     * @param  {object} window
3834
     * @param  {object} document
3835
     * @class
3836
     */
3837
    var Controller = (function (window, document) {
3838
        var me = {};
3839
3840
        /**
3841
         * hides all status messages no matter which module showed them
3842
         *
3843
         * @name   Controller.hideStatusMessages
3844
         * @function
3845
         */
3846
        me.hideStatusMessages = function()
3847
        {
3848
            PasteStatus.hideMessages();
3849
            Alert.hideMessages();
3850
        }
3851
3852
        /**
3853
         * creates a new paste
3854
         *
3855
         * @name   Controller.newPaste
3856
         * @function
3857
         */
3858
        me.newPaste = function()
3859
        {
3860
            // Important: This *must not* run Alert.hideMessages() as previous
3861
            // errors from viewing a paste should be shown.
3862
            TopNav.hideAllButtons();
3863
            Alert.showLoading('Preparing new paste…', 0, 'time');
3864
3865
            PasteStatus.hideMessages();
3866
            PasteViewer.hide();
3867
            Editor.resetInput();
3868
            Editor.show();
3869
            Editor.focusInput();
3870
3871
            TopNav.loadDefaults();
3872
            TopNav.showCreateButtons();
3873
            Alert.hideLoading();
3874
        }
3875
3876
        /**
3877
         * shows the loaded paste
3878
         *
3879
         * @name   Controller.showPaste
3880
         * @function
3881
         */
3882
        me.showPaste = function()
3883
        {
3884
            try {
3885
                Model.getPasteId();
3886
                Model.getPasteKey();
3887
            } catch (err) {
3888
                console.error(err);
3889
3890
                // missing decryption key (or paste ID) in URL?
3891
                if (window.location.hash.length === 0) {
3892
                    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?)');
3893
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3894
                    return;
3895
                }
3896
            }
3897
3898
            // show proper elements on screen
3899
            PasteDecrypter.run();
3900
            return;
0 ignored issues
show
Unused Code introduced by
This return has no effect and can be removed.
Loading history...
3901
        }
3902
3903
        /**
3904
         * refreshes the loaded paste to show potential new data
3905
         *
3906
         * @name   Controller.refreshPaste
3907
         * @function
3908
         * @param {function} callback
3909
         */
3910
        me.refreshPaste = function(callback)
3911
        {
3912
            // save window position to restore it later
3913
            var orgPosition = $(window).scrollTop();
3914
3915
            Uploader.prepare();
3916
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3917
3918
            Uploader.setFailure(function (status, data) {
3919
                // revert loading status…
3920
                Alert.hideLoading();
3921
                TopNav.showViewButtons();
3922
3923
                // show error message
3924
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3925
            })
3926
            Uploader.setSuccess(function (status, data) {
3927
                PasteDecrypter.run(data);
3928
3929
                // restore position
3930
                window.scrollTo(0, orgPosition);
3931
3932
                callback();
3933
            })
3934
            Uploader.run();
3935
        }
3936
3937
        /**
3938
         * clone the current paste
3939
         *
3940
         * @name   Controller.clonePaste
3941
         * @function
3942
         * @param  {Event} event
3943
         */
3944
        me.clonePaste = function(event)
0 ignored issues
show
Unused Code introduced by
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...
3945
        {
3946
            TopNav.collapseBar();
3947
            TopNav.hideAllButtons();
3948
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3949
3950
            // hide messages from previous paste
3951
            me.hideStatusMessages();
3952
3953
            // erase the id and the key in url
3954
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3955
3956
            if (AttachmentViewer.hasAttachment()) {
3957
                AttachmentViewer.moveAttachmentTo(
3958
                    TopNav.getCustomAttachment(),
3959
                    'Cloned: \'%s\''
3960
                );
3961
                TopNav.hideFileSelector();
3962
                AttachmentViewer.hideAttachment();
3963
                // NOTE: it also looks nice without removing the attachment
3964
                // but for a consistent display we remove it…
3965
                AttachmentViewer.hideAttachmentPreview();
3966
                TopNav.showCustomAttachment();
3967
3968
                // show another status message to make the user aware that the
3969
                // file was cloned too!
3970
                Alert.showStatus(
3971
                    [
3972
                        'The cloned file \'%s\' was attached to this paste.',
3973
                        AttachmentViewer.getAttachment()[1]
3974
                    ], 'copy', true, true);
3975
            }
3976
3977
            Editor.setText(PasteViewer.getText())
3978
            PasteViewer.hide();
3979
            Editor.show();
3980
3981
            Alert.hideLoading();
3982
            TopNav.showCreateButtons();
3983
        }
3984
3985
        /**
3986
         * removes a saved paste
3987
         *
3988
         * @name   Controller.removePaste
3989
         * @function
3990
         * @param  {string} pasteId
3991
         * @param  {string} deleteToken
3992
         */
3993
        me.removePaste = function(pasteId, deleteToken) {
3994
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3995
            // so we use a POST request
3996
            Uploader.prepare();
3997
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3998
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3999
4000
            Uploader.setFailure(function () {
4001
                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
4002
            })
4003
            Uploader.run();
4004
        }
4005
4006
        /**
4007
         * application start
4008
         *
4009
         * @name   Controller.init
4010
         * @function
4011
         */
4012
        me.init = function()
4013
        {
4014
            // first load translations
4015
            I18n.loadTranslations();
4016
4017
            // initialize other modules/"classes"
4018
            Alert.init();
4019
            Model.init();
4020
4021
            AttachmentViewer.init();
4022
            CryptTool.init();
4023
            DiscussionViewer.init();
4024
            Editor.init();
4025
            PasteDecrypter.init();
4026
            PasteEncrypter.init();
4027
            PasteStatus.init();
4028
            PasteViewer.init();
4029
            Prompt.init();
4030
            TopNav.init();
4031
            UiHelper.init();
4032
            Uploader.init();
4033
4034
            // display an existing paste
4035
            if (Model.hasCipherData()) {
4036
                return me.showPaste();
4037
            }
4038
4039
            // otherwise create a new paste
4040
            me.newPaste();
0 ignored issues
show
Best Practice introduced by
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...
4041
        }
4042
4043
        return me;
4044
    })(window, document);
4045
4046
    return {
4047
        Helper: Helper,
4048
        I18n: I18n,
4049
        CryptTool: CryptTool,
4050
        Model: Model,
4051
        UiHelper: UiHelper,
4052
        Alert: Alert,
4053
        PasteStatus: PasteStatus,
4054
        Prompt: Prompt,
4055
        Editor: Editor,
4056
        PasteViewer: PasteViewer,
4057
        AttachmentViewer: AttachmentViewer,
4058
        DiscussionViewer: DiscussionViewer,
4059
        TopNav: TopNav,
4060
        Uploader: Uploader,
4061
        PasteEncrypter: PasteEncrypter,
4062
        PasteDecrypter: PasteDecrypter,
4063
        Controller: Controller
4064
    };
4065
}(jQuery, sjcl, Base64, RawDeflate);
4066