Passed
Pull Request — master (#180)
by El
03:33
created

me.submitPaste   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 10
nop 0
dl 0
loc 71
rs 6.7968
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
1000
        var currentIcon = [
1001
            'glyphicon-time', // loading icon
1002
            'glyphicon-info-sign', // status icon
1003
            '', // resevered for warning, not used yet
1004
            'glyphicon-alert' // error icon
1005
        ];
1006
1007
        var alertType = [
1008
            'loading', // not in bootstrap, but using a good value here
1009
            'info', // status icon
1010
            'warning', // not used yet
1011
            'danger' // error icon
1012
        ];
1013
1014
        var customHandler;
1015
1016
        /**
1017
         * forwards a request to the i18n module and shows the element
1018
         *
1019
         * @private
1020
         * @function
1021
         * @param  {int} id - id of notification
1022
         * @param  {jQuery} $element - jQuery object
1023
         * @param  {string|array} args
1024
         * @param  {string|null} icon - optional, icon
1025
         */
1026
        function handleNotification(id, $element, args, icon)
1027
        {
1028
            // basic parsing/conversion of parameters
1029
            if (typeof icon === 'undefined') {
1030
                icon = null;
1031
            }
1032
            if (typeof args === 'undefined') {
1033
                args = null;
1034
            } else if (typeof args === 'string') {
1035
                // convert string to array if needed
1036
                args = [args];
1037
            }
1038
1039
            // pass to custom handler if dfined
1040
            if (typeof customHandler === 'function') {
1041
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1042
                if (handlerResult === true) {
1043
                    // if it returs true, skip own handler
1044
                    return;
1045
                }
1046
                if (handlerResult instanceof jQuery) {
1047
                    // continue processing with new element
1048
                    $element = handlerResult;
1049
                    icon = null; // icons not supported in this case
1050
                }
1051
            }
1052
1053
            // handle icon
1054
            if (icon !== null && // icon was passed
1055
                icon !== currentIcon[id] // and it differs from current icon
1056
            ) {
1057
                var $glyphIcon = $element.find(':first');
1058
1059
                // remove (previous) icon
1060
                $glyphIcon.removeClass(currentIcon[id]);
1061
1062
                // any other thing as a string (e.g. 'null') (only) removes the icon
1063
                if (typeof icon === 'string') {
1064
                    // set new icon
1065
                    currentIcon[id] = 'glyphicon-' + icon;
1066
                    $glyphIcon.addClass(currentIcon[id]);
1067
                }
1068
            }
1069
1070
            // show text
1071
            if (args !== null) {
1072
                // add jQuery object to it as first parameter
1073
                args.unshift($element.find(':last'));
1074
1075
                // pass it to I18n
1076
                I18n._.apply(this, args);
1077
            }
1078
1079
            // show notification
1080
            $element.removeClass('hidden');
1081
        }
1082
1083
        /**
1084
         * display a status message
1085
         *
1086
         * This automatically passes the text to I18n for translation.
1087
         *
1088
         * @name   Alert.showStatus
1089
         * @function
1090
         * @param  {string|array} message     string, use an array for %s/%d options
1091
         * @param  {string|null}  icon        optional, the icon to show,
1092
         *                                    default: leave previous icon
1093
         * @param  {bool}         dismissable optional, whether the notification
1094
         *                                    can be dismissed (closed), default: false
1095
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1096
         *                                    notification should be hidden automatically;
1097
         *                                    default: disabled (0); use true for default value
1098
         */
1099
        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...
1100
        {
1101
            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...
1102
            // @TODO: implement dismissable
1103
            // @TODO: implement autoclose
1104
1105
            handleNotification(1, $statusMessage, message, icon);
1106
        }
1107
1108
        /**
1109
         * display an error message
1110
         *
1111
         * This automatically passes the text to I18n for translation.
1112
         *
1113
         * @name   Alert.showError
1114
         * @function
1115
         * @param  {string|array} message     string, use an array for %s/%d options
1116
         * @param  {string|null}  icon        optional, the icon to show, default:
1117
         *                                    leave previous icon
1118
         * @param  {bool}         dismissable optional, whether the notification
1119
         *                                    can be dismissed (closed), default: false
1120
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1121
         *                                    notification should be hidden automatically;
1122
         *                                    default: disabled (0); use true for default value
1123
         */
1124
        me.showError = 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...
1125
        {
1126
            console.error('error message shown: ', message);
1127
            // @TODO: implement dismissable (bootstrap add-on has it)
1128
            // @TODO: implement autoclose
1129
1130
            handleNotification(3, $errorMessage, message, icon);
1131
        }
1132
1133
        /**
1134
         * shows a loading message, optionally with a percentage
1135
         *
1136
         * This automatically passes all texts to the i10s module.
1137
         *
1138
         * @name   Alert.showLoading
1139
         * @function
1140
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1141
         * @param  {int}               percentage   optional, default: null
1142
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1143
         */
1144
        me.showLoading = function(message, percentage, icon)
1145
        {
1146
            if (typeof message !== 'undefined' && message !== null) {
1147
                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...
1148
            }
1149
1150
            // default message text
1151
            if (typeof message === 'undefined') {
1152
                message = 'Loading…';
1153
            }
1154
1155
            // currently percentage parameter is ignored
1156
            // // @TODO handle it here…
1157
1158
            handleNotification(0, $loadingIndicator, message, icon);
1159
1160
            // show loading status (cursor)
1161
            $('body').addClass('loading');
1162
        }
1163
1164
        /**
1165
         * hides the loading message
1166
         *
1167
         * @name   Alert.hideLoading
1168
         * @function
1169
         */
1170
        me.hideLoading = function()
1171
        {
1172
            $loadingIndicator.addClass('hidden');
1173
1174
            // hide loading cursor
1175
            $('body').removeClass('loading');
1176
        }
1177
1178
        /**
1179
         * hides any status/error messages
1180
         *
1181
         * This does not include the loading message.
1182
         *
1183
         * @name   Alert.hideMessages
1184
         * @function
1185
         */
1186
        me.hideMessages = function()
1187
        {
1188
            // also possible: $('.statusmessage').addClass('hidden');
1189
            $statusMessage.addClass('hidden');
1190
            $errorMessage.addClass('hidden');
1191
        }
1192
1193
        /**
1194
         * set a custom handler, which gets all notifications.
1195
         *
1196
         * This handler gets the following arguments:
1197
         * alertType (see array), $element, args, icon
1198
         * If it returns true, the own processing will be stopped so the message
1199
         * will not be displayed. Otherwise it will continue.
1200
         * As an aditional feature it can return q jQuery element, which will
1201
         * then be used to add the message there. Icons are not supported in
1202
         * that case and will be ignored.
1203
         * Pass 'null' to reset/delete the custom handler.
1204
         * Note that there is no notification when a message is supposed to get
1205
         * hidden.
1206
         *
1207
         * @name   Alert.setCustomHandler
1208
         * @function
1209
         * @param {function|null} newHandler
1210
         */
1211
        me.setCustomHandler = function(newHandler)
1212
        {
1213
            customHandler = newHandler;
1214
        }
1215
1216
        /**
1217
         * init status manager
1218
         *
1219
         * preloads jQuery elements
1220
         *
1221
         * @name   Alert.init
1222
         * @function
1223
         */
1224
        me.init = function()
1225
        {
1226
            // hide "no javascript" error message
1227
            $('#noscript').hide();
1228
1229
            // not a reset, but first set of the elements
1230
            $errorMessage = $('#errormessage');
1231
            $loadingIndicator = $('#loadingindicator');
1232
            $statusMessage = $('#status');
1233
1234
            // display status returned by php code, if any (e.g. paste was properly deleted)
1235
            var serverStatus = $statusMessage.text();
1236
            if (Helper.isValidText(serverStatus)) {
1237
                me.showStatus();
1238
            }
1239
1240
            // display error message from php code
1241
            var serverError = $errorMessage.text();
1242
            if (Helper.isValidText(serverError)) {
1243
                Alert.showError();
1244
            }
1245
        }
1246
1247
        return me;
1248
    })(window, document);
1249
1250
    /**
1251
     * handles paste status/result
1252
     *
1253
     * @param  {object} window
1254
     * @param  {object} document
1255
     * @class
1256
     */
1257
    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...
1258
        var me = {};
1259
1260
        var $pasteSuccess,
1261
            $pasteUrl,
1262
            $remainingTime,
1263
            $shortenButton;
1264
1265
        /**
1266
         * forward to URL shortener
1267
         *
1268
         * @private
1269
         * @function
1270
         * @param  {Event} event
1271
         */
1272
        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...
1273
        {
1274
            window.location.href = $shortenButton.data('shortener')
1275
                                   + encodeURIComponent($pasteUrl.attr('href'));
1276
        }
1277
1278
        /**
1279
         * Forces opening the paste if the link does not do this automatically.
1280
         *
1281
         * This is necessary as browsers will not reload the page when it is
1282
         * already loaded (which is fake as it is set via history.pushState()).
1283
         *
1284
         * @name   Controller.pasteLinkClick
1285
         * @function
1286
         * @param  {Event} event
1287
         */
1288
        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...
1289
        {
1290
            // check if location is (already) shown in URL bar
1291
            if (window.location.href === $pasteUrl.attr('href')) {
1292
                // if so we need to load link by reloading the current site
1293
                window.location.reload(true);
1294
            }
1295
        }
1296
1297
        /**
1298
         * creates a notification after a successfull paste upload
1299
         *
1300
         * @name   PasteStatus.createPasteNotification
1301
         * @function
1302
         * @param  {string} url
1303
         * @param  {string} deleteUrl
1304
         */
1305
        me.createPasteNotification = function(url, deleteUrl)
1306
        {
1307
            $('#pastelink').html(
1308
                I18n._(
1309
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1310
                    url, url
1311
                )
1312
            );
1313
            // save newly created element
1314
            $pasteUrl = $('#pasteurl');
1315
            // and add click event
1316
            $pasteUrl.click(pasteLinkClick);
1317
1318
            // shorten button
1319
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1320
1321
            // show result
1322
            $pasteSuccess.removeClass('hidden');
1323
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1324
            Helper.selectText($pasteUrl[0]);
1325
        }
1326
1327
        /**
1328
         * shows the remaining time
1329
         *
1330
         * @name PasteStatus.showRemainingTime
1331
         * @function
1332
         * @param {object} pasteMetaData
1333
         */
1334
        me.showRemainingTime = function(pasteMetaData)
1335
        {
1336
            if (pasteMetaData.burnafterreading) {
1337
                // display paste "for your eyes only" if it is deleted
1338
1339
                // actually remove paste, before we claim it is deleted
1340
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1341
1342
                I18n._($remainingTime.find(':last'), "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1343
                $remainingTime.addClass('foryoureyesonly');
1344
1345
                // discourage cloning (it cannot really be prevented)
1346
                TopNav.hideCloneButton();
1347
1348
            } else if (pasteMetaData.expire_date) {
1349
                // display paste expiration
1350
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1351
                    expirationLabel = [
1352
                        'This document will expire in %d ' + expiration[1] + '.',
1353
                        'This document will expire in %d ' + expiration[1] + 's.'
1354
                    ];
1355
1356
                I18n._($remainingTime.find(':last'), expirationLabel, expiration[0]);
1357
                $remainingTime.removeClass('foryoureyesonly')
1358
            } else {
1359
                // never expires
1360
                return;
1361
            }
1362
1363
            // in the end, display notification
1364
            $remainingTime.removeClass('hidden');
1365
        }
1366
1367
        /**
1368
         * hides the remaining time and successful upload notification
1369
         *
1370
         * @name PasteStatus.hideRemainingTime
1371
         * @function
1372
         */
1373
        me.hideMessages = function()
1374
        {
1375
            $remainingTime.addClass('hidden');
1376
            $pasteSuccess.addClass('hidden');
1377
        }
1378
1379
        /**
1380
         * init status manager
1381
         *
1382
         * preloads jQuery elements
1383
         *
1384
         * @name   Alert.init
1385
         * @function
1386
         */
1387
        me.init = function()
1388
        {
1389
            $pasteSuccess = $('#pasteSuccess');
1390
            // $pasteUrl is saved in me.createPasteNotification() after creation
1391
            $remainingTime = $('#remainingtime');
1392
            $shortenButton = $('#shortenbutton');
1393
1394
            // bind elements
1395
            $shortenButton.click(sendToShortener);
1396
        }
1397
1398
        return me;
1399
    })(window, document);
1400
1401
    /**
1402
     * password prompt
1403
     *
1404
     * @param  {object} window
1405
     * @param  {object} document
1406
     * @class
1407
     */
1408
    var Prompt = (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...
1409
        var me = {};
1410
1411
        var $passwordDecrypt,
1412
            $passwordForm,
1413
            $passwordModal;
1414
1415
        var password = '',
1416
            passwordCallback = null;
1417
1418
        /**
1419
         * ask the user for the password and set it
1420
         *
1421
         * the callback set via setPasswordCallback is executed
1422
         *
1423
         * @name Prompt.requestPassword()
1424
         * @function
1425
         */
1426
        me.requestPassword = function()
1427
        {
1428
            // show new bootstrap method (if available)
1429
            if ($passwordModal.length !== 0) {
1430
                $passwordModal.modal({
1431
                    backdrop: 'static',
1432
                    keyboard: false
1433
                });
1434
1435
                return;
1436
            }
1437
1438
            // fallback to old method for page template
1439
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1440
            if (newPassword === null) {
1441
                throw 'password prompt canceled';
1442
            }
1443
            if (password.length === 0) {
1444
                // recursive…
1445
                return me.requestPassword();
1446
            }
1447
1448
            password = newPassword;
1449
1450
            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...
1451
                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...
1452
            }
1453
        }
1454
1455
        /**
1456
         * getthe cached password
1457
         *
1458
         * If you do not get a password with this function
1459
         * (returns an empty string), use requestPassword.
1460
         *
1461
         * @name   Prompt.getPassword
1462
         * @function
1463
         * @return {string}
1464
         */
1465
        me.getPassword = function()
1466
        {
1467
            return password;
1468
        }
1469
1470
        /**
1471
         * setsthe callback called when password is entered
1472
         *
1473
         * @name   Prompt.setPasswordCallback
1474
         * @function
1475
         * @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...
1476
         */
1477
        me.setPasswordCallback = function(callback)
1478
        {
1479
            passwordCallback = callback;
1480
        }
1481
1482
        /**
1483
         * submit a password in the modal dialog
1484
         *
1485
         * @private
1486
         * @function
1487
         * @param  {Event} event
1488
         */
1489
        function submitPasswordModal(event)
1490
        {
1491
            // get input
1492
            password = $passwordDecrypt.val();
1493
1494
            // hide modal
1495
            $passwordModal.modal('hide');
1496
1497
            if (passwordCallback !== null) {
1498
                passwordCallback();
1499
            }
1500
1501
            event.preventDefault();
1502
        }
1503
1504
1505
        /**
1506
         * init status manager
1507
         *
1508
         * preloads jQuery elements
1509
         *
1510
         * @name   Controller.init
1511
         * @function
1512
         */
1513
        me.init = function()
1514
        {
1515
            $passwordDecrypt = $('#passworddecrypt');
1516
            $passwordForm = $('#passwordform');
1517
            $passwordModal = $('#passwordmodal');
1518
1519
            // bind events
1520
1521
            // focus password input when it is shown
1522
            $passwordModal.on('shown.bs.Model', function () {
1523
                $passwordDecrypt.focus();
1524
            });
1525
            // handle Model password submission
1526
            $passwordForm.submit(submitPasswordModal);
1527
        }
1528
1529
        return me;
1530
    })(window, document);
1531
1532
    /**
1533
     * Manage paste/message input, and preview tab
1534
     *
1535
     * Note that the actual preview is handled by PasteViewer.
1536
     *
1537
     * @param  {object} window
1538
     * @param  {object} document
1539
     * @class
1540
     */
1541
    var Editor = (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...
1542
        var me = {};
1543
1544
        var $editorTabs,
1545
            $messageEdit,
1546
            $messagePreview,
1547
            $message;
1548
1549
        var isPreview = false;
1550
1551
        /**
1552
         * support input of tab character
1553
         *
1554
         * @name   Editor.supportTabs
1555
         * @function
1556
         * @param  {Event} event
1557
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1558
         */
1559
        function supportTabs(event)
1560
        {
1561
            var keyCode = event.keyCode || event.which;
1562
            // tab was pressed
1563
            if (keyCode === 9) {
1564
                // get caret position & selection
1565
                var val   = this.value,
1566
                    start = this.selectionStart,
1567
                    end   = this.selectionEnd;
1568
                // set textarea value to: text before caret + tab + text after caret
1569
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1570
                // put caret at right position again
1571
                this.selectionStart = this.selectionEnd = start + 1;
1572
                // prevent the textarea to lose focus
1573
                event.preventDefault();
1574
            }
1575
        }
1576
1577
        /**
1578
         * view the Editor tab
1579
         *
1580
         * @name   Editor.viewEditor
1581
         * @function
1582
         * @param  {Event} event - optional
1583
         */
1584
        function viewEditor(event)
1585
        {
1586
            // toggle buttons
1587
            $messageEdit.addClass('active');
1588
            $messagePreview.removeClass('active');
1589
1590
            PasteViewer.hide();
1591
1592
            // reshow input
1593
            $message.removeClass('hidden');
1594
1595
            me.focusInput();
1596
1597
            // finish
1598
            isPreview = false;
1599
1600
            // prevent jumping of page to top
1601
            if (typeof event !== 'undefined') {
1602
                event.preventDefault();
1603
            }
1604
        }
1605
1606
        /**
1607
         * view the preview tab
1608
         *
1609
         * @name   Editor.viewPreview
1610
         * @function
1611
         * @param  {Event} event
1612
         */
1613
        function viewPreview(event)
1614
        {
1615
            // toggle buttons
1616
            $messageEdit.removeClass('active');
1617
            $messagePreview.addClass('active');
1618
1619
            // hide input as now preview is shown
1620
            $message.addClass('hidden');
1621
1622
            // show preview
1623
            $('#errormessage').find(':last')
1624
            PasteViewer.setText($message.val());
1625
            PasteViewer.run();
1626
1627
            // finish
1628
            isPreview = true;
1629
1630
            // prevent jumping of page to top
1631
            if (typeof event !== 'undefined') {
1632
                event.preventDefault();
1633
            }
1634
        }
1635
1636
        /**
1637
         * get the state of the preview
1638
         *
1639
         * @name   Editor.isPreview
1640
         * @function
1641
         */
1642
        me.isPreview = function()
1643
        {
1644
            return isPreview;
1645
        }
1646
1647
        /**
1648
         * reset the Editor view
1649
         *
1650
         * @name   Editor.resetInput
1651
         * @function
1652
         */
1653
        me.resetInput = function()
1654
        {
1655
            // go back to input
1656
            if (isPreview) {
1657
                viewEditor();
1658
            }
1659
1660
            // clear content
1661
            $message.val('');
1662
        }
1663
1664
        /**
1665
         * shows the Editor
1666
         *
1667
         * @name   Editor.show
1668
         * @function
1669
         */
1670
        me.show = function()
1671
        {
1672
            $message.removeClass('hidden');
1673
            $editorTabs.removeClass('hidden');
1674
        }
1675
1676
        /**
1677
         * hides the Editor
1678
         *
1679
         * @name   Editor.reset
1680
         * @function
1681
         */
1682
        me.hide = function()
1683
        {
1684
            $message.addClass('hidden');
1685
            $editorTabs.addClass('hidden');
1686
        }
1687
1688
        /**
1689
         * focuses the message input
1690
         *
1691
         * @name   Editor.focusInput
1692
         * @function
1693
         */
1694
        me.focusInput = function()
1695
        {
1696
            $message.focus();
1697
        }
1698
1699
        /**
1700
         * sets a new text
1701
         *
1702
         * @name   Editor.setText
1703
         * @function
1704
         * @param {string} newText
1705
         */
1706
        me.setText = function(newText)
1707
        {
1708
            $message.val(newText);
1709
        }
1710
1711
        /**
1712
         * returns the current text
1713
         *
1714
         * @name   Editor.getText
1715
         * @function
1716
         * @return {string}
1717
         */
1718
        me.getText = function()
1719
        {
1720
            return $message.val()
1721
        }
1722
1723
        /**
1724
         * init status manager
1725
         *
1726
         * preloads jQuery elements
1727
         *
1728
         * @name   Editor.init
1729
         * @function
1730
         */
1731
        me.init = function()
1732
        {
1733
            $editorTabs = $('#editorTabs');
1734
            $message = $('#message');
1735
1736
            // bind events
1737
            $message.keydown(supportTabs);
1738
1739
            // bind click events to tab switchers (a), but save parent of them
1740
            // (li)
1741
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1742
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1743
        }
1744
1745
        return me;
1746
    })(window, document);
1747
1748
    /**
1749
     * (view) Parse and show paste.
1750
     *
1751
     * @param  {object} window
1752
     * @param  {object} document
1753
     * @class
1754
     */
1755
    var PasteViewer = (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...
1756
        var me = {};
1757
1758
        var $placeholder,
1759
            $prettyMessage,
1760
            $prettyPrint,
1761
            $plainText;
1762
1763
        var text,
1764
            format = 'plaintext',
1765
            isDisplayed = false,
1766
            isChanged = true; // by default true as nothing was parsed yet
1767
1768
        /**
1769
         * apply the set format on paste and displays it
1770
         *
1771
         * @private
1772
         * @function
1773
         */
1774
        function parsePaste()
1775
        {
1776
            // skip parsing if no text is given
1777
            if (text === '') {
1778
                return;
1779
            }
1780
1781
            // set text
1782
            Helper.setElementText($plainText, text);
1783
            Helper.setElementText($prettyPrint, text);
1784
1785
            switch (format) {
1786
                case 'markdown':
1787
                    var converter = new showdown.Converter({
1788
                        strikethrough: true,
1789
                        tables: true,
1790
                        tablesHeaderId: true
1791
                    });
1792
                    $plainText.html(
1793
                        converter.makeHtml(text)
1794
                    );
1795
                    // add table classes from bootstrap css
1796
                    $plainText.find('table').addClass('table-condensed table-bordered');
1797
                    break;
1798
                case 'syntaxhighlighting':
1799
                    // @TODO is this really needed or is "one" enough?
1800
                    if (typeof prettyPrint === 'function')
1801
                    {
1802
                        prettyPrint();
1803
                    }
1804
1805
                    $prettyPrint.html(
1806
                        prettyPrintOne(
1807
                            Helper.htmlEntities(text), null, true
1808
                        )
1809
                    );
1810
                    // fall through, as the rest is the same
1811
                default: // = 'plaintext'
1812
                    // convert URLs to clickable links
1813
                    Helper.urls2links($plainText);
1814
                    Helper.urls2links($prettyPrint);
1815
1816
                    $prettyPrint.css('white-space', 'pre-wrap');
1817
                    $prettyPrint.css('word-break', 'normal');
1818
                    $prettyPrint.removeClass('prettyprint');
1819
            }
1820
        }
1821
1822
        /**
1823
         * displays the paste
1824
         *
1825
         * @private
1826
         * @function
1827
         */
1828
        function showPaste()
1829
        {
1830
            // instead of "nothing" better display a placeholder
1831
            if (text === '') {
1832
                $placeholder.removeClass('hidden')
1833
                return;
1834
            }
1835
            // otherwise hide the placeholder
1836
            $placeholder.addClass('hidden')
1837
1838
            switch (format) {
1839
                case 'markdown':
1840
                    $plainText.removeClass('hidden');
1841
                    $prettyMessage.addClass('hidden');
1842
                    break;
1843
                default:
1844
                    $plainText.addClass('hidden');
1845
                    $prettyMessage.removeClass('hidden');
1846
                    break;
1847
            }
1848
        }
1849
1850
        /**
1851
         * sets the format in which the text is shown
1852
         *
1853
         * @name   PasteViewer.setFormat
1854
         * @function
1855
         * @param {string} newFormat the the new format
1856
         */
1857
        me.setFormat = function(newFormat)
1858
        {
1859
            // skip if there is no update
1860
            if (format === newFormat) {
1861
                return;
1862
            }
1863
1864
            // needs to update display too, if from or to Markdown is switched
1865
            if (format === 'markdown' || newFormat === 'markdown') {
1866
                isDisplayed = false;
1867
            }
1868
1869
            format = newFormat;
1870
            isChanged = true;
1871
        }
1872
1873
        /**
1874
         * returns the current format
1875
         *
1876
         * @name   PasteViewer.setFormat
1877
         * @function
1878
         * @return {string}
1879
         */
1880
        me.getFormat = function()
1881
        {
1882
            return format;
1883
        }
1884
1885
        /**
1886
         * returns whether the current view is pretty printed
1887
         *
1888
         * @name   PasteViewer.isPrettyPrinted
1889
         * @function
1890
         * @return {bool}
1891
         */
1892
        me.isPrettyPrinted = function()
1893
        {
1894
            return $prettyPrint.hasClass('prettyprinted');
1895
        }
1896
1897
        /**
1898
         * sets the text to show
1899
         *
1900
         * @name   PasteViewer.setText
1901
         * @function
1902
         * @param {string} newText the text to show
1903
         */
1904
        me.setText = function(newText)
1905
        {
1906
            if (text !== newText) {
1907
                text = newText;
1908
                isChanged = true;
1909
            }
1910
        }
1911
1912
        /**
1913
         * gets the current cached text
1914
         *
1915
         * @name   PasteViewer.getText
1916
         * @function
1917
         * @return {string}
1918
         */
1919
        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...
1920
        {
1921
            return text;
1922
        }
1923
1924
        /**
1925
         * show/update the parsed text (preview)
1926
         *
1927
         * @name   PasteViewer.run
1928
         * @function
1929
         */
1930
        me.run = function()
1931
        {
1932
            if (isChanged) {
1933
                parsePaste();
1934
                isChanged = false;
1935
            }
1936
1937
            if (!isDisplayed) {
1938
                showPaste();
1939
                isDisplayed = true;
1940
            }
1941
        }
1942
1943
        /**
1944
         * hide parsed text (preview)
1945
         *
1946
         * @name   PasteViewer.hide
1947
         * @function
1948
         */
1949
        me.hide = function()
1950
        {
1951
            if (!isDisplayed) {
1952
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1953
            }
1954
1955
            $plainText.addClass('hidden');
1956
            $prettyMessage.addClass('hidden');
1957
            $placeholder.addClass('hidden');
1958
1959
            isDisplayed = false;
1960
        }
1961
1962
        /**
1963
         * init status manager
1964
         *
1965
         * preloads jQuery elements
1966
         *
1967
         * @name   Editor.init
1968
         * @function
1969
         */
1970
        me.init = function()
1971
        {
1972
            $placeholder = $('#placeholder');
1973
            $plainText = $('#plaintext');
1974
            $prettyMessage = $('#prettymessage');
1975
            $prettyPrint = $('#prettyprint');
1976
1977
            // check requirements
1978
            if (typeof prettyPrintOne !== 'function') {
1979
                Alert.showError([
1980
                    'The library %s is not available. This may cause display errors.',
1981
                    'pretty print'
1982
                ]);
1983
            }
1984
            if (typeof showdown !== 'object') {
1985
                Alert.showError([
1986
                    'The library %s is not available. This may cause display errors.',
1987
                    'showdown'
1988
                ]);
1989
            }
1990
1991
            // get default option from template/HTML or fall back to set value
1992
            format = Model.getFormatDefault() || format;
1993
        }
1994
1995
        return me;
1996
    })(window, document);
1997
1998
    /**
1999
     * (view) Show attachment and preview if possible
2000
     *
2001
     * @param  {object} window
2002
     * @param  {object} document
2003
     * @class
2004
     */
2005
    var AttachmentViewer = (function (window, document) {
2006
        var me = {};
2007
2008
        var $attachmentLink,
2009
            $attachmentPreview,
2010
            $attachment;
2011
2012
        var attachmentChanged = false,
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2013
            attachmentHasPreview = false;
2014
2015
        /**
2016
         * sets the attachment but does not yet show it
2017
         *
2018
         * @name   AttachmentViewer.setAttachment
2019
         * @function
2020
         * @param {string} attachmentData - base64-encoded data of file
2021
         * @param {string} fileName - optional, file name
2022
         */
2023
        me.setAttachment = function(attachmentData, fileName)
2024
        {
2025
            var imagePrefix = 'data:image/';
2026
2027
            $attachmentLink.attr('href', attachmentData);
2028
            if (typeof fileName !== 'undefined') {
2029
                $attachmentLink.attr('download', fileName);
2030
            }
2031
2032
            // if the attachment is an image, display it
2033
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2034
                $attachmentPreview.html(
2035
                    $(document.createElement('img'))
2036
                        .attr('src', attachmentData)
2037
                        .attr('class', 'img-thumbnail')
2038
                );
2039
                attachmentHasPreview = true;
2040
            }
2041
2042
            attachmentChanged = true;
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2043
        }
2044
2045
        /**
2046
         * displays the attachment
2047
         *
2048
         * @name AttachmentViewer.showAttachment
2049
         * @function
2050
         */
2051
        me.showAttachment = function()
2052
        {
2053
            $attachment.removeClass('hidden');
2054
2055
            if (attachmentHasPreview) {
2056
                $attachmentPreview.removeClass('hidden');
2057
            }
2058
        }
2059
2060
        /**
2061
         * removes the attachment
2062
         *
2063
         * This automatically hides the attachment containers to, to
2064
         * prevent an inconsistent display.
2065
         *
2066
         * @name AttachmentViewer.removeAttachment
2067
         * @function
2068
         */
2069
        me.removeAttachment = function()
2070
        {
2071
            me.hideAttachment();
2072
            me.hideAttachmentPreview();
2073
            $attachmentLink.prop('href', '');
2074
            $attachmentLink.prop('download', '');
2075
            $attachmentPreview.html('');
2076
        }
2077
2078
        /**
2079
         * hides the attachment
2080
         *
2081
         * This will not hide the preview {@see me.hideAttachmentPreview}
2082
         * nor will it hide the attachment link if it was moved somewhere
2083
         * else {@see moveAttachmentTo}.
2084
         *
2085
         * @name AttachmentViewer.hideAttachment
2086
         * @function
2087
         */
2088
        me.hideAttachment = function()
2089
        {
2090
            $attachment.addClass('hidden');
2091
        }
2092
2093
        /**
2094
         * hides the attachment preview
2095
         *
2096
         * @name AttachmentViewer.hideAttachmentPreview
2097
         * @function
2098
         */
2099
        me.hideAttachmentPreview = function()
2100
        {
2101
            $attachmentPreview.addClass('hidden');
2102
        }
2103
2104
        /**
2105
         * checks if there is an attachment
2106
         *
2107
         * @name   AttachmentViewer.hasAttachment
2108
         * @function
2109
         */
2110
        me.hasAttachment = function()
2111
        {
2112
            return ($attachmentLink.prop('href') !== '')
2113
        }
2114
2115
        /**
2116
         * return the attachment
2117
         *
2118
         * @name   AttachmentViewer.getAttachment
2119
         * @function
2120
         * @returns {array}
2121
         */
2122
        me.getAttachment = function()
2123
        {
2124
            return [
2125
                $attachmentLink.prop('href'),
2126
                $attachmentLink.prop('download')
2127
            ];
2128
        }
2129
2130
        /**
2131
         * moves the attachment link to another element
2132
         *
2133
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2134
         *
2135
         * @name   AttachmentViewer.setClonedAttachment
2136
         * @function
2137
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2138
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2139
         */
2140
        me.moveAttachmentTo = function($element, label)
2141
        {
2142
            // move elemement to new place
2143
            $attachmentLink.appendTo($element);
2144
2145
            // update text
2146
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2147
        }
2148
2149
        /**
2150
         * initiate
2151
         *
2152
         * preloads jQuery elements
2153
         *
2154
         * @name   AttachmentViewer.init
2155
         * @function
2156
         */
2157
        me.init = function()
2158
        {
2159
            $attachment = $('#attachment');
2160
            $attachmentLink = $('#attachment a');
2161
            $attachmentPreview = $('#attachmentPreview');
2162
        }
2163
2164
        return me;
2165
    })(window, document);
2166
2167
    /**
2168
     * (view) Shows discussion thread and handles replies
2169
     *
2170
     * @param  {object} window
2171
     * @param  {object} document
2172
     * @class
2173
     */
2174
    var DiscussionViewer = (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...
2175
        var me = {};
2176
2177
        var $commentTail,
2178
            $discussion,
2179
            $reply,
2180
            $replyMessage,
2181
            $replyNickname,
2182
            $replyStatus,
2183
            $commentContainer;
2184
2185
        var replyCommentId;
2186
2187
        /**
2188
         * initializes the templates
2189
         *
2190
         * @private
2191
         * @function
2192
         */
2193
        function initTemplates()
2194
        {
2195
            $reply = Model.getTemplate('reply');
2196
            $replyMessage = $reply.find('#replymessage');
2197
            $replyNickname = $reply.find('#nickname');
2198
            $replyStatus = $reply.find('#replystatus');
2199
2200
            // cache jQuery elements
2201
            $commentTail = Model.getTemplate('commenttail');
2202
        }
2203
2204
        /**
2205
         * custom handler for displaying notifications in own status message area
2206
         *
2207
         * @name   DiscussionViewer.handleNotification
2208
         * @function
2209
         * @param  {string} alertType
2210
         * @param  {jQuery} $element
2211
         * @param  {string|array} args
2212
         * @param  {string|null} icon
2213
         * @return {bool|jQuery}
2214
         */
2215
        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 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...
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...
2216
        {
2217
            // ignore loading messages
2218
            if (alertType === 'loading') {
2219
                return false;
2220
            }
2221
2222
            if (alertType === 'danger') {
2223
                $replyStatus.removeClass('alert-info');
2224
                $replyStatus.addClass('alert-danger');
2225
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2226
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2227
            } else {
2228
                $replyStatus.removeClass('alert-danger');
2229
                $replyStatus.addClass('alert-info');
2230
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2231
                $replyStatus.find(':first').addClass('glyphicon-alert');
2232
            }
2233
2234
            return $replyStatus;
2235
        }
2236
2237
        /**
2238
         * open the comment entry when clicking the "Reply" button of a comment
2239
         *
2240
         * @private
2241
         * @function
2242
         * @param  {Event} event
2243
         */
2244
        function openReply(event)
2245
        {
2246
            var $source = $(event.target);
2247
2248
            // clear input
2249
            $replyMessage.val('');
2250
            $replyNickname.val('');
2251
2252
            // get comment id from source element
2253
            replyCommentId = $source.parent().prop('id').split('_')[1];
2254
2255
            // move to correct position
2256
            $source.after($reply);
2257
2258
            // show
2259
            $reply.removeClass('hidden');
2260
            $replyMessage.focus();
2261
2262
            event.preventDefault();
2263
        }
2264
2265
        /**
2266
         * adds another comment
2267
         *
2268
         * @name   DiscussionViewer.addComment
2269
         * @function
2270
         * @param {object} comment
2271
         * @param {string} commentText
2272
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2273
         */
2274
        me.addComment = function(comment, commentText, nickname, $place)
2275
        {
2276
            if (typeof $place === 'undefined') {
2277
                // starting point (default value/fallback)
2278
                $place = $commentContainer;
2279
2280
                // if parent comment exists
2281
                var $parentComment = $('#comment_' + comment.parentid);
2282
                if ($parentComment.length) {
2283
                    // use parent as position for noew comment, so it shifted
2284
                    // to the right
2285
                    $place = $parentComment;
2286
                }
2287
            }
2288
            if (commentText === '') {
2289
                commentText = 'comment decryption failed';
2290
            }
2291
2292
            // create new comment based on template
2293
            var $commentEntry = Model.getTemplate('comment');
2294
            $commentEntry.prop('id', 'comment_' + comment.id);
2295
            var $commentEntryData = $commentEntry.find('div.commentdata');
2296
2297
            // set & parse text
2298
            Helper.setElementText($commentEntryData, commentText);
2299
            Helper.urls2links($commentEntryData);
2300
2301
            // set nickname
2302
            if (nickname.length > 0){
2303
                $commentEntry.find('span.nickname').text(nickname);
2304
            } else {
2305
                $commentEntry.find('span.nickname').html('<i>' + I18n._('Anonymous') + '</i>');
2306
            }
2307
2308
            // set date
2309
            $commentEntry.find('span.commentdate')
2310
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2311
                      .attr('title', 'CommentID: ' + comment.id);
2312
2313
            // if an avatar is available, display it
2314
            if (comment.meta.vizhash) {
2315
                $commentEntry.find('span.nickname')
2316
                          .before(
2317
                            '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
2318
                            I18n._('Avatar generated from IP address') + '" /> '
2319
                          );
2320
            }
2321
2322
            // finally append comment
2323
            $place.append($commentEntry);
2324
        }
2325
2326
        /**
2327
         * finishes the discussion area after last comment
2328
         *
2329
         * @name   DiscussionViewer.finishDiscussion
2330
         * @function
2331
         */
2332
        me.finishDiscussion = function()
2333
        {
2334
            // add 'add new comment' area
2335
            $commentContainer.append($commentTail);
2336
2337
            // show discussions
2338
            $discussion.removeClass('hidden');
2339
        }
2340
2341
        /**
2342
         * shows the discussion area
2343
         *
2344
         * @name   DiscussionViewer.showDiscussion
2345
         * @function
2346
         */
2347
        me.showDiscussion = function()
2348
        {
2349
            $discussion.removeClass('hidden');
2350
        }
2351
2352
        /**
2353
         * removes the old discussion and prepares everything for creating a new
2354
         * one.
2355
         *
2356
         * @name   DiscussionViewer.prepareNewDisucssion
2357
         * @function
2358
         */
2359
        me.prepareNewDisucssion = function()
2360
        {
2361
            $commentContainer.html('');
2362
            $discussion.addClass('hidden');
2363
2364
            // (re-)init templates
2365
            initTemplates();
2366
        }
2367
2368
        /**
2369
         * returns the user put into the reply form
2370
         *
2371
         * @name   DiscussionViewer.getReplyData
2372
         * @function
2373
         * @return {array}
2374
         */
2375
        me.getReplyData = function()
2376
        {
2377
            return [
2378
                $replyMessage.val(),
2379
                $replyNickname.val()
2380
            ];
2381
        }
2382
2383
        /**
2384
         * highlights a specific comment and scrolls to it if necessary
2385
         *
2386
         * @name   DiscussionViewer.highlightComment
2387
         * @function
2388
         * @param {string} commentId
2389
         * @param {bool} fadeOut - whether to fade out the comment
2390
         */
2391
        me.highlightComment = function(commentId, fadeOut)
2392
        {
2393
            var $comment = $('#comment_' + commentId);
2394
            // in case comment does not exist, cancel
2395
            if ($comment.length === 0) {
2396
                return;
2397
            }
2398
2399
            var highlightComment = function () {
2400
                $comment.addClass('highlight');
2401
                if (fadeOut === true) {
2402
                    setTimeout(function () {
2403
                        $comment.removeClass('highlight');
2404
                    }, 300);
2405
                }
2406
            }
2407
2408
            if (UiHelper.isVisible($comment)) {
2409
                return highlightComment();
2410
            }
2411
2412
            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...
2413
        }
2414
2415
        /**
2416
         * returns the id of the parent comment the user is replying to
2417
         *
2418
         * @name   DiscussionViewer.getReplyCommentId
2419
         * @function
2420
         * @return {int|undefined}
2421
         */
2422
        me.getReplyCommentId = function()
2423
        {
2424
            return replyCommentId;
2425
        }
2426
2427
        /**
2428
         * initiate
2429
         *
2430
         * preloads jQuery elements
2431
         *
2432
         * @name   DiscussionViewer.init
2433
         * @function
2434
         */
2435
        me.init = function()
2436
        {
2437
            // bind events to templates (so they are later cloned)
2438
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2439
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2440
2441
            $commentContainer = $('#commentcontainer');
2442
            $discussion = $('#discussion');
2443
        }
2444
2445
        return me;
2446
    })(window, document);
2447
2448
    /**
2449
     * Manage top (navigation) bar
2450
     *
2451
     * @param {object} window
2452
     * @param {object} document
2453
     * @class
2454
     */
2455
    var TopNav = (function (window, document) {
2456
        var me = {};
2457
2458
        var createButtonsDisplayed = false;
2459
        var viewButtonsDisplayed = false;
2460
2461
        var $attach,
2462
            $burnAfterReading,
2463
            $burnAfterReadingOption,
2464
            $cloneButton,
2465
            $customAttachment,
2466
            $expiration,
2467
            $fileRemoveButton,
2468
            $fileWrap,
2469
            $formatter,
2470
            $newButton,
2471
            $openDiscussion,
2472
            $openDiscussionOption,
2473
            $password,
2474
            $passwordInput,
2475
            $rawTextButton,
2476
            $sendButton;
2477
2478
        var pasteExpiration = '1week';
2479
2480
        /**
2481
         * set the expiration on bootstrap templates in dropdown
2482
         *
2483
         * @name   TopNav.updateExpiration
2484
         * @function
2485
         * @param  {Event} event
2486
         */
2487
        function updateExpiration(event)
2488
        {
2489
            // get selected option
2490
            var target = $(event.target);
2491
2492
            // update dropdown display and save new expiration time
2493
            $('#pasteExpirationDisplay').text(target.text());
2494
            pasteExpiration = target.data('expiration');
2495
2496
            event.preventDefault();
2497
        }
2498
2499
        /**
2500
         * set the format on bootstrap templates in dropdown
2501
         *
2502
         * @name   TopNav.updateFormat
2503
         * @function
2504
         * @param  {Event} event
2505
         */
2506
        function updateFormat(event)
2507
        {
2508
            // get selected option
2509
            var $target = $(event.target);
2510
2511
            // update dropdown display and save new format
2512
            var newFormat = $target.data('format');
2513
            $('#pasteFormatterDisplay').text($target.text());
2514
            PasteViewer.setFormat(newFormat);
2515
2516
            // update preview
2517
            if (Editor.isPreview()) {
2518
                PasteViewer.run();
2519
            }
2520
2521
            event.preventDefault();
2522
        }
2523
2524
        /**
2525
         * when "burn after reading" is checked, disable discussion
2526
         *
2527
         * @name   TopNav.changeBurnAfterReading
2528
         * @function
2529
         */
2530
        function changeBurnAfterReading()
2531
        {
2532
            if ($burnAfterReading.is(':checked')) {
2533
                $openDiscussionOption.addClass('buttondisabled');
2534
                $openDiscussion.prop('checked', false);
2535
2536
                // if button is actually disabled, force-enable it and uncheck other button
2537
                $burnAfterReadingOption.removeClass('buttondisabled');
2538
            } else {
2539
                $openDiscussionOption.removeClass('buttondisabled');
2540
            }
2541
        }
2542
2543
        /**
2544
         * when discussion is checked, disable "burn after reading"
2545
         *
2546
         * @name   TopNav.changeOpenDiscussion
2547
         * @function
2548
         */
2549
        function changeOpenDiscussion()
2550
        {
2551
            if ($openDiscussion.is(':checked')) {
2552
                $burnAfterReadingOption.addClass('buttondisabled');
2553
                $burnAfterReading.prop('checked', false);
2554
2555
                // if button is actually disabled, force-enable it and uncheck other button
2556
                $openDiscussionOption.removeClass('buttondisabled');
2557
            } else {
2558
                $burnAfterReadingOption.removeClass('buttondisabled');
2559
            }
2560
        }
2561
2562
        /**
2563
         * return raw text
2564
         *
2565
         * @name   TopNav.rawText
2566
         * @function
2567
         * @param  {Event} event
2568
         */
2569
        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...
2570
        {
2571
            TopNav.hideAllButtons();
2572
            Alert.showLoading('Showing raw text…', 0, 'time');
2573
            var paste = PasteViewer.getText();
2574
2575
            // push a new state to allow back navigation with browser back button
2576
            history.pushState(
2577
                {type: 'raw'},
2578
                document.title,
2579
                // recreate paste URL
2580
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2581
                Model.getPasteKey()
2582
            );
2583
2584
            // we use text/html instead of text/plain to avoid a bug when
2585
            // reloading the raw text view (it reverts to type text/html)
2586
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2587
            var newDoc = document.open('text/html', 'replace');
2588
            newDoc.write('<!DOCTYPE html><html><head>');
2589
            for (var i = 0; i < $head.length; i++) {
2590
                newDoc.write($head[i].outerHTML);
2591
            }
2592
            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
2593
            newDoc.close();
2594
        }
2595
2596
        /**
2597
         * saves the language in a cookie and reloads the page
2598
         *
2599
         * @name   TopNav.setLanguage
2600
         * @function
2601
         * @param  {Event} event
2602
         */
2603
        function setLanguage(event)
2604
        {
2605
            document.cookie = 'lang=' + $(event.target).data('lang');
2606
            UiHelper.reloadHome();
2607
        }
2608
2609
        /**
2610
         * hides all messages and creates a new paste
2611
         *
2612
         * @private
2613
         * @function
2614
         * @param  {Event} event
2615
         */
2616
        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...
2617
        {
2618
            Controller.hideStatusMessages();
2619
            Controller.newPaste();
2620
        }
2621
2622
        /**
2623
         * removes the existing attachment
2624
         *
2625
         * @private
2626
         * @function
2627
         * @param  {Event} event
2628
         */
2629
        function removeAttachment(event)
2630
        {
2631
            // if custom attachment is used, remove it first
2632
            if (!$customAttachment.hasClass('hidden')) {
2633
                AttachmentViewer.removeAttachment();
2634
                $customAttachment.addClass('hidden');
2635
                $fileWrap.removeClass('hidden');
2636
            }
2637
2638
            // our up-to-date jQuery can handle it :)
2639
            $fileWrap.find('input').val('');
2640
2641
            // pevent '#' from appearing in the URL
2642
            event.preventDefault();
2643
        }
2644
2645
        /**
2646
         * Loads the default options for creating a paste.
2647
         *
2648
         * @name   TopNav.loadDefaults
2649
         * @function
2650
         */
2651
        me.loadDefaults = function()
2652
        {
2653
            // @TODO
2654
        }
2655
2656
        /**
2657
         * Shows all elements belonging to viwing an existing pastes
2658
         *
2659
         * @name   TopNav.showViewButtons
2660
         * @function
2661
         */
2662
        me.showViewButtons = function()
2663
        {
2664
            if (viewButtonsDisplayed) {
2665
                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...
2666
                return;
2667
            }
2668
2669
            $newButton.removeClass('hidden');
2670
            $cloneButton.removeClass('hidden');
2671
            $rawTextButton.removeClass('hidden');
2672
2673
            viewButtonsDisplayed = true;
2674
        }
2675
2676
        /**
2677
         * Hides all elements belonging to existing pastes
2678
         *
2679
         * @name   TopNav.hideViewButtons
2680
         * @function
2681
         */
2682
        me.hideViewButtons = function()
2683
        {
2684
            if (!viewButtonsDisplayed) {
2685
                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...
2686
                return;
2687
            }
2688
2689
            $newButton.addClass('hidden');
2690
            $cloneButton.addClass('hidden');
2691
            $rawTextButton.addClass('hidden');
2692
2693
            viewButtonsDisplayed = false;
2694
        }
2695
2696
        /**
2697
         * Hides all elements belonging to existing pastes
2698
         *
2699
         * @name   TopNav.hideAllButtons
2700
         * @function
2701
         */
2702
        me.hideAllButtons = function()
2703
        {
2704
            me.hideViewButtons();
2705
            me.hideCreateButtons();
2706
        }
2707
2708
        /**
2709
         * shows all elements needed when creating a new paste
2710
         *
2711
         * @name   TopNav.showCreateButtons
2712
         * @function
2713
         */
2714
        me.showCreateButtons = function()
2715
        {
2716
            if (createButtonsDisplayed) {
2717
                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...
2718
                return;
2719
            }
2720
2721
            $sendButton.removeClass('hidden');
2722
            $expiration.removeClass('hidden');
2723
            $formatter.removeClass('hidden');
2724
            $burnAfterReadingOption.removeClass('hidden');
2725
            $openDiscussionOption.removeClass('hidden');
2726
            $newButton.removeClass('hidden');
2727
            $password.removeClass('hidden');
2728
            $attach.removeClass('hidden');
2729
2730
            createButtonsDisplayed = true;
2731
        }
2732
2733
        /**
2734
         * shows all elements needed when creating a new paste
2735
         *
2736
         * @name   TopNav.hideCreateButtons
2737
         * @function
2738
         */
2739
        me.hideCreateButtons = function()
2740
        {
2741
            if (!createButtonsDisplayed) {
2742
                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...
2743
                return;
2744
            }
2745
2746
            $newButton.addClass('hidden');
2747
            $sendButton.addClass('hidden');
2748
            $expiration.addClass('hidden');
2749
            $formatter.addClass('hidden');
2750
            $burnAfterReadingOption.addClass('hidden');
2751
            $openDiscussionOption.addClass('hidden');
2752
            $password.addClass('hidden');
2753
            $attach.addClass('hidden');
2754
2755
            createButtonsDisplayed = false;
2756
        }
2757
2758
        /**
2759
         * only shows the "new paste" button
2760
         *
2761
         * @name   TopNav.showNewPasteButton
2762
         * @function
2763
         */
2764
        me.showNewPasteButton = function()
2765
        {
2766
            $newButton.removeClass('hidden');
2767
        }
2768
2769
        /**
2770
         * only hides the clone button
2771
         *
2772
         * @name   TopNav.hideCloneButton
2773
         * @function
2774
         */
2775
        me.hideCloneButton = function()
2776
        {
2777
            $cloneButton.addClass('hidden');
2778
        }
2779
2780
        /**
2781
         * only hides the raw text button
2782
         *
2783
         * @name   TopNav.hideRawButton
2784
         * @function
2785
         */
2786
        me.hideRawButton = function()
2787
        {
2788
            $rawTextButton.addClass('hidden');
2789
        }
2790
2791
        /**
2792
         * hides the file selector in attachment
2793
         *
2794
         * @name   TopNav.hideFileSelector
2795
         * @function
2796
         */
2797
        me.hideFileSelector = function()
2798
        {
2799
            $fileWrap.addClass('hidden');
2800
        }
2801
2802
2803
        /**
2804
         * shows the custom attachment
2805
         *
2806
         * @name   TopNav.showCustomAttachment
2807
         * @function
2808
         */
2809
        me.showCustomAttachment = function()
2810
        {
2811
            $customAttachment.removeClass('hidden');
2812
        }
2813
2814
        /**
2815
         * collapses the navigation bar if nedded
2816
         *
2817
         * @name   TopNav.collapseBar
2818
         * @function
2819
         */
2820
        me.collapseBar = function()
2821
        {
2822
            var $bar = $('.navbar-toggle');
2823
2824
            // check if bar is expanded
2825
            if ($bar.hasClass('collapse in')) {
2826
                // if so, toggle it
2827
                $bar.click();
2828
            }
2829
        }
2830
2831
        /**
2832
         * returns the currently set expiration time
2833
         *
2834
         * @name   TopNav.getExpiration
2835
         * @function
2836
         * @return {int}
2837
         */
2838
        me.getExpiration = function()
2839
        {
2840
            return pasteExpiration;
2841
        }
2842
2843
        /**
2844
         * returns the currently selected file(s)
2845
         *
2846
         * @name   TopNav.getFileList
2847
         * @function
2848
         * @return {FileList|null}
2849
         */
2850
        me.getFileList = function()
2851
        {
2852
            var $file = $('#file');
2853
2854
            // if no file given, return null
2855
            if (!$file.length || !$file[0].files.length) {
2856
                return null;
2857
            }
2858
            // @TODO is this really necessary
2859
            if (!($file[0].files && $file[0].files[0])) {
2860
                return null;
2861
            }
2862
2863
            return $file[0].files;
2864
        }
2865
2866
        /**
2867
         * returns the state of the burn after reading checkbox
2868
         *
2869
         * @name   TopNav.getExpiration
2870
         * @function
2871
         * @return {bool}
2872
         */
2873
        me.getBurnAfterReading = function()
2874
        {
2875
            return $burnAfterReading.is(':checked');
2876
        }
2877
2878
        /**
2879
         * returns the state of the discussion checkbox
2880
         *
2881
         * @name   TopNav.getOpenDiscussion
2882
         * @function
2883
         * @return {bool}
2884
         */
2885
        me.getOpenDiscussion = function()
2886
        {
2887
            return $openDiscussion.is(':checked');
2888
        }
2889
2890
        /**
2891
         * returns the entered password
2892
         *
2893
         * @name   TopNav.getPassword
2894
         * @function
2895
         * @return {string}
2896
         */
2897
        me.getPassword = function()
2898
        {
2899
            return $passwordInput.val();
2900
        }
2901
2902
        /**
2903
         * returns the element where custom attachments can be placed
2904
         *
2905
         * Used by AttachmentViewer when an attachment is cloned here.
2906
         *
2907
         * @name   TopNav.getCustomAttachment
2908
         * @function
2909
         * @return {jQuery}
2910
         */
2911
        me.getCustomAttachment = function()
2912
        {
2913
            return $customAttachment;
2914
        }
2915
2916
        /**
2917
         * init navigation manager
2918
         *
2919
         * preloads jQuery elements
2920
         *
2921
         * @name   TopNav.init
2922
         * @function
2923
         */
2924
        me.init = function()
2925
        {
2926
            $attach = $('#attach');
2927
            $burnAfterReading = $('#burnafterreading');
2928
            $burnAfterReadingOption = $('#burnafterreadingoption');
2929
            $cloneButton = $('#clonebutton');
2930
            $customAttachment = $('#customattachment');
2931
            $expiration = $('#expiration');
2932
            $fileRemoveButton = $('#fileremovebutton');
2933
            $fileWrap = $('#filewrap');
2934
            $formatter = $('#formatter');
2935
            $newButton = $('#newbutton');
2936
            $openDiscussion = $('#opendiscussion');
2937
            $openDiscussionOption = $('#opendiscussionoption');
2938
            $password = $('#password');
2939
            $passwordInput = $('#passwordinput');
2940
            $rawTextButton = $('#rawtextbutton');
2941
            $sendButton = $('#sendbutton');
2942
2943
            // bootstrap template drop down
2944
            $('#language ul.dropdown-menu li a').click(setLanguage);
2945
            // page template drop down
2946
            $('#language select option').click(setLanguage);
2947
2948
            // bind events
2949
            $burnAfterReading.change(changeBurnAfterReading);
2950
            $openDiscussionOption.change(changeOpenDiscussion);
2951
            $newButton.click(clickNewPaste);
2952
            $sendButton.click(PasteEncrypter.submitPaste);
2953
            $cloneButton.click(Controller.clonePaste);
2954
            $rawTextButton.click(rawText);
2955
            $fileRemoveButton.click(removeAttachment);
2956
2957
            // bootstrap template drop downs
2958
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2959
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2960
2961
            // initiate default state of checkboxes
2962
            changeBurnAfterReading();
2963
            changeOpenDiscussion();
2964
2965
            // get default value from template or fall back to set value
2966
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2967
2968
            me.loadDefaults();
2969
        }
2970
2971
        return me;
2972
    })(window, document);
2973
2974
    /**
2975
     * Responsible for AJAX requests, transparently handles encryption…
2976
     *
2977
     * @class
2978
     */
2979
    var Uploader = (function () {
2980
        var me = {};
2981
2982
        var successFunc = null,
2983
            failureFunc = null,
2984
            url,
2985
            data,
2986
            symmetricKey,
2987
            password;
2988
2989
        /**
2990
         * public variable ('constant') for errors to prevent magic numbers
2991
         *
2992
         * @readonly
2993
         * @enum   {Object}
2994
         */
2995
        me.error = {
2996
            okay: 0,
2997
            custom: 1,
2998
            unknown: 2,
2999
            serverError: 3
3000
        };
3001
3002
        /**
3003
         * ajaxHeaders to send in AJAX requests
3004
         *
3005
         * @private
3006
         * @readonly
3007
         * @enum   {Object}
3008
         */
3009
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
3010
3011
        /**
3012
         * called after successful upload
3013
         *
3014
         * @private
3015
         * @function
3016
         * @throws {string}
3017
         */
3018
        function checkCryptParameters()
3019
        {
3020
            // workaround for this nasty 'bug' in ECMAScript
3021
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3022
            var typeOfKey = typeof symmetricKey;
3023
            if (symmetricKey === null) {
3024
                typeOfKey = 'null';
3025
            }
3026
3027
            // in case of missing preparation, throw error
3028
            switch (typeOfKey) {
3029
                case 'string':
3030
                    // already set, all right
3031
                    return;
3032
                case 'null':
3033
                    // needs to be generated auto-generate
3034
                    symmetricKey = CryptTool.getSymmetricKey();
3035
                    break;
3036
                default:
3037
                    console.error('current invalid symmetricKey:', symmetricKey);
3038
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3039
            }
3040
            // password is optional
3041
        }
3042
3043
        /**
3044
         * called after successful upload
3045
         *
3046
         * @private
3047
         * @function
3048
         * @param {int} status
3049
         * @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...
3050
         */
3051
        function success(status, result)
3052
        {
3053
            // add useful data to result
3054
            result.encryptionKey = symmetricKey;
3055
            result.requestData = data;
3056
3057
            if (successFunc !== null) {
3058
                successFunc(status, result);
3059
            }
3060
        }
3061
3062
        /**
3063
         * called after a upload failure
3064
         *
3065
         * @private
3066
         * @function
3067
         * @param {int} status - internal code
3068
         * @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...
3069
         */
3070
        function fail(status, result)
3071
        {
3072
            if (failureFunc !== null) {
3073
                failureFunc(status, result);
3074
            }
3075
        }
3076
3077
        /**
3078
         * actually uploads the data
3079
         *
3080
         * @name   Uploader.run
3081
         * @function
3082
         */
3083
        me.run = function()
3084
        {
3085
            $.ajax({
3086
                type: 'POST',
3087
                url: url,
3088
                data: data,
3089
                dataType: 'json',
3090
                headers: ajaxHeaders,
3091
                success: function(result) {
3092
                    if (result.status === 0) {
3093
                        success(0, result);
3094
                    } else if (result.status === 1) {
3095
                        fail(1, result);
3096
                    } else {
3097
                        fail(2, result);
3098
                    }
3099
                }
3100
            })
3101
            .fail(function(jqXHR, textStatus, errorThrown) {
3102
                console.error(textStatus, errorThrown);
3103
                fail(3, jqXHR);
3104
            });
3105
        }
3106
3107
        /**
3108
         * set success function
3109
         *
3110
         * @name   Uploader.setSuccess
3111
         * @function
3112
         * @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...
3113
         */
3114
        me.setUrl = function(newUrl)
3115
        {
3116
            url = newUrl;
3117
        }
3118
3119
        /**
3120
         * sets the password to use (first value) and optionally also the
3121
         * encryption key (not recommend, it is automatically generated).
3122
         *
3123
         * Note: Call this after prepare() as prepare() resets these values.
3124
         *
3125
         * @name   Uploader.setCryptValues
3126
         * @function
3127
         * @param {string} newPassword
3128
         * @param {string} newKey       - optional
3129
         */
3130
        me.setCryptParameters = function(newPassword, newKey)
3131
        {
3132
            password = newPassword;
3133
3134
            if (typeof newKey !== 'undefined') {
3135
                symmetricKey = newKey;
3136
            }
3137
        }
3138
3139
        /**
3140
         * set success function
3141
         *
3142
         * @name   Uploader.setSuccess
3143
         * @function
3144
         * @param {function} func
3145
         */
3146
        me.setSuccess = function(func)
3147
        {
3148
            successFunc = func;
3149
        }
3150
3151
        /**
3152
         * set failure function
3153
         *
3154
         * @name   Uploader.setSuccess
3155
         * @function
3156
         * @param {function} func
3157
         */
3158
        me.setFailure = function(func)
3159
        {
3160
            failureFunc = func;
3161
        }
3162
3163
        /**
3164
         * prepares a new upload
3165
         *
3166
         * Call this when doing a new upload to reset any data from potential
3167
         * previous uploads. Must be called before any other method of this
3168
         * module.
3169
         *
3170
         * @name   Uploader.prepare
3171
         * @function
3172
         * @return {object}
3173
         */
3174
        me.prepare = function()
3175
        {
3176
            // entropy should already be checked!
3177
3178
            // reset password
3179
            password = '';
3180
3181
            // reset key, so it a new one is generated when it is used
3182
            symmetricKey = null;
3183
3184
            // reset data
3185
            successFunc = null;
3186
            failureFunc = null;
3187
            url = Helper.baseUri();
3188
            data = {};
3189
        }
3190
3191
        /**
3192
         * encrypts and sets the data
3193
         *
3194
         * @name   Uploader.setData
3195
         * @function
3196
         * @param {string} index
3197
         * @param {mixed} element
3198
         */
3199
        me.setData = function(index, element)
3200
        {
3201
            checkCryptParameters();
3202
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3203
        }
3204
3205
        /**
3206
         * set the additional metadata to send unencrypted
3207
         *
3208
         * @name   Uploader.setUnencryptedData
3209
         * @function
3210
         * @param {string} index
3211
         * @param {mixed} element
3212
         */
3213
        me.setUnencryptedData = function(index, element)
3214
        {
3215
            data[index] = element;
3216
        }
3217
3218
        /**
3219
         * set the additional metadata to send unencrypted passed at once
3220
         *
3221
         * @name   Uploader.setUnencryptedData
3222
         * @function
3223
         * @param {object} newData
3224
         */
3225
        me.setUnencryptedBulkData = function(newData)
3226
        {
3227
            $.extend(data, newData);
3228
        }
3229
3230
        /**
3231
         * Helper, which parses shows a general error message based on the result of the Uploader
3232
         *
3233
         * @name    Uploader.parseUploadError
3234
         * @function
3235
         * @param {int} status
3236
         * @param {object} data
3237
         * @param {string} doThisThing - a human description of the action, which was tried
3238
         * @return {array}
3239
         */
3240
        me.parseUploadError = function(status, data, doThisThing) {
3241
            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...
3242
3243
            switch (status) {
3244
                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...
3245
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3246
                    break;
3247
                case Uploader.error['unknown']:
3248
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3249
                    break;
3250
                case Uploader.error['serverError']:
3251
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];                        break;
3252
                default:
3253
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3254
                    break;
3255
            }
3256
3257
            return errorArray;
3258
        }
3259
3260
        /**
3261
         * init Uploader
3262
         *
3263
         * @name   Uploader.init
3264
         * @function
3265
         */
3266
        me.init = function()
3267
        {
3268
            // nothing yet
3269
        }
3270
3271
        return me;
3272
    })();
3273
3274
    /**
3275
     * (controller) Responsible for encrypting paste and sending it to server.
3276
     *
3277
     * Does upload, encryption is done transparently by Uploader.
3278
     *
3279
     * @name state
3280
     * @class
3281
     */
3282
    var PasteEncrypter = (function () {
3283
        var me = {};
3284
3285
        var requirementsChecked = false;
3286
3287
        /**
3288
         * checks whether there is a suitable amount of entrophy
3289
         *
3290
         * @private
3291
         * @function
3292
         * @param {function} retryCallback - the callback to execute to retry the upload
3293
         * @return {bool}
3294
         */
3295
        function checkRequirements(retryCallback) {
3296
            // skip double requirement checks
3297
            if (requirementsChecked === true) {
3298
                return true;
3299
            }
3300
3301
            if (!CryptTool.isEntropyReady()) {
3302
                // display a message and wait
3303
                Alert.showStatus('Please move your mouse for more entropy…');
3304
3305
                CryptTool.addEntropySeedListener(retryCallback);
3306
                return false;
3307
            }
3308
3309
            requirementsChecked = true;
3310
3311
            return true;
3312
        }
3313
3314
        /**
3315
         * called after successful paste upload
3316
         *
3317
         * @private
3318
         * @function
3319
         * @param {int} status
3320
         * @param {object} data
3321
         */
3322
        function showCreatedPaste(status, data) {
3323
            Alert.hideLoading();
3324
3325
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3326
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3327
3328
            Alert.hideMessages();
3329
3330
            // show notification
3331
            PasteStatus.createPasteNotification(url, deleteUrl)
3332
3333
            // show new URL in browser bar
3334
            history.pushState({type: 'newpaste'}, document.title, url);
3335
3336
            TopNav.showViewButtons();
3337
            TopNav.hideRawButton();
3338
            Editor.hide();
3339
3340
            // parse and show text
3341
            // (preparation already done in me.submitPaste())
3342
            PasteViewer.run();
3343
        }
3344
3345
        /**
3346
         * called after successful comment upload
3347
         *
3348
         * @private
3349
         * @function
3350
         * @param {int} status
3351
         * @param {object} data
3352
         */
3353
        function showUploadedComment(status, data) {
3354
            // show success message
3355
            // Alert.showStatus('Comment posted.');
3356
3357
            // reload paste
3358
            Controller.refreshPaste(function () {
3359
                // highlight sent comment
3360
                DiscussionViewer.highlightComment(data.id, true);
3361
                // reset error handler
3362
                Alert.setCustomHandler(null);
3363
            });
3364
        }
3365
3366
        /**
3367
         * adds attachments to the Uploader
3368
         *
3369
         * @private
3370
         * @function
3371
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3372
         * @param {function} callback - excuted when action is successful
3373
         */
3374
        function encryptAttachments(file, callback) {
3375
            if (typeof file !== 'undefined' && file !== null) {
3376
                // check file reader requirements for upload
3377
                if (typeof FileReader === 'undefined') {
3378
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3379
                    // cancels process as it does not execute callback
3380
                    return;
3381
                }
3382
3383
                var reader = new FileReader();
3384
3385
                // closure to capture the file information
3386
                reader.onload = function(event) {
3387
                    Uploader.setData('attachment', event.target.result);
3388
                    Uploader.setData('attachmentname', file.name);
3389
3390
                    // run callback
3391
                    return callback();
3392
                }
3393
3394
                // actually read first file
3395
                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...
3396
            } else if (AttachmentViewer.hasAttachment()) {
3397
                // fall back to cloned part
3398
                var attachment = AttachmentViewer.getAttachment();
3399
3400
                Uploader.setData('attachment', attachment[0]);
3401
                Uploader.setData('attachmentname', attachment[1]);
3402
                return callback();
3403
            } else {
3404
                // if there are no attachments, this is of course still successful
3405
                return callback();
3406
            }
3407
        }
3408
3409
        /**
3410
         * send a reply in a discussion
3411
         *
3412
         * @name   PasteEncrypter.sendComment
3413
         * @function
3414
         */
3415
        me.sendComment = function()
3416
        {
3417
            Alert.hideMessages();
3418
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3419
3420
            // UI loading state
3421
            TopNav.hideAllButtons();
3422
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3423
3424
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3425
            var replyData = DiscussionViewer.getReplyData(),
3426
                plainText = replyData[0],
3427
                nickname = replyData[1],
3428
                parentid = DiscussionViewer.getReplyCommentId();
3429
3430
            // do not send if there is no data
3431
            if (plainText.length === 0) {
3432
                // revert loading status…
3433
                Alert.hideLoading();
3434
                Alert.setCustomHandler(null);
3435
                TopNav.showViewButtons();
3436
                return;
3437
            }
3438
3439
            // check entropy
3440
            if (!checkRequirements(function () {
3441
                me.sendComment();
3442
            })) {
3443
                return; // to prevent multiple executions
3444
            }
3445
            Alert.showLoading(null, 10);
3446
3447
            // prepare Uploader
3448
            Uploader.prepare();
3449
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3450
3451
            // set success/fail functions
3452
            Uploader.setSuccess(showUploadedComment);
3453
            Uploader.setFailure(function (status, data) {
3454
                // revert loading status…
3455
                Alert.hideLoading();
3456
                TopNav.showViewButtons();
3457
3458
                // show error message
3459
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3460
3461
                // reset error handler
3462
                Alert.setCustomHandler(null);
3463
            });
3464
3465
            // fill it with unencrypted params
3466
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3467
            if (typeof parentid === 'undefined') {
3468
                // if parent id is not set, this is the top-most comment, so use
3469
                // paste id as parent @TODO is this really good?
3470
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3471
            } else {
3472
                Uploader.setUnencryptedData('parentid', parentid);
3473
            }
3474
3475
            // encrypt data
3476
            Uploader.setData('data', plainText);
3477
3478
            if (nickname.length > 0) {
3479
                Uploader.setData('nickname', nickname);
3480
            }
3481
3482
            Uploader.run();
3483
        }
3484
3485
        /**
3486
         * sends a new paste to server
3487
         *
3488
         * @name   PasteEncrypter.submitPaste
3489
         * @function
3490
         */
3491
        me.submitPaste = function()
3492
        {
3493
            // hide previous (error) messages
3494
            Controller.hideStatusMessages();
3495
3496
            // UI loading state
3497
            TopNav.hideAllButtons();
3498
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3499
            TopNav.collapseBar();
3500
3501
            // get data
3502
            var plainText = Editor.getText(),
3503
                format = PasteViewer.getFormat(),
3504
                files = TopNav.getFileList();
3505
3506
            // do not send if there is no data
3507
            if (plainText.length === 0 && files === null) {
3508
                // revert loading status…
3509
                Alert.hideLoading();
3510
                TopNav.showCreateButtons();
3511
                return;
3512
            }
3513
3514
            Alert.showLoading(null, 10);
3515
3516
            // check entropy
3517
            if (!checkRequirements(function () {
3518
                me.submitPaste();
3519
            })) {
3520
                return; // to prevent multiple executions
3521
            }
3522
3523
            // prepare Uploader
3524
            Uploader.prepare();
3525
            Uploader.setCryptParameters(TopNav.getPassword());
3526
3527
            // set success/fail functions
3528
            Uploader.setSuccess(showCreatedPaste);
3529
            Uploader.setFailure(function (status, data) {
3530
                // revert loading status…
3531
                Alert.hideLoading();
3532
                TopNav.showCreateButtons();
3533
3534
                // show error message
3535
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3536
            });
3537
3538
            // fill it with unencrypted submitted options
3539
            Uploader.setUnencryptedBulkData({
3540
                expire:           TopNav.getExpiration(),
3541
                formatter:        format,
3542
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3543
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3544
            });
3545
3546
            // prepare PasteViewer for later preview
3547
            PasteViewer.setText(plainText);
3548
            PasteViewer.setFormat(format);
3549
3550
            // encrypt cipher data
3551
            Uploader.setData('data', plainText);
3552
3553
            // encrypt attachments
3554
            encryptAttachments(
3555
                files === null ? null : files[0],
3556
                function () {
3557
                    // send data
3558
                    Uploader.run();
3559
                }
3560
            );
3561
        }
3562
3563
        /**
3564
         * initialize
3565
         *
3566
         * @name   PasteEncrypter.init
3567
         * @function
3568
         */
3569
        me.init = function()
3570
        {
3571
            // nothing yet
3572
        }
3573
3574
        return me;
3575
    })();
3576
3577
    /**
3578
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3579
     *
3580
     * Only decryption, no download.
3581
     *
3582
     * @name state
3583
     * @class
3584
     */
3585
    var PasteDecrypter = (function () {
3586
        var me = {};
3587
3588
        /**
3589
         * decrypt data or prompts for password in cvase of failure
3590
         *
3591
         * @private
3592
         * @function
3593
         * @param {string} key
3594
         * @param {string} password - optional, may be an empty string
3595
         * @param {string} cipherdata
3596
         * @throws {string}
3597
         * @return {false|string} - false, when unsuccessful or string (decrypted data)
3598
         */
3599
        function decryptOrPromptPassword(key, password, cipherdata)
3600
        {
3601
            // try decryption without password
3602
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3603
3604
            // if it fails, request password
3605
            if (plaindata.length === 0 && password.length === 0) {
3606
                // try to get cached password first
3607
                password = Prompt.getPassword();
3608
3609
                // if password is there, re-try
3610
                if (password.length !== 0) {
3611
                    // recursive
3612
                    // note: an infinite loop is prevented as the previous if
3613
                    // clause checks whether a password is already set and ignores
3614
                    // errors when a password has been passed
3615
                    return decryptOrPromptPassword.apply(arguments);
3616
                }
3617
3618
                // trigger password request
3619
                Prompt.requestPassword();
3620
                // the callback (via setPasswordCallback()) should have been set
3621
                // by a parent function
3622
                return false;
3623
            }
3624
3625
            // if all tries failed, we can only return an error
3626
            if (plaindata.length === 0) {
3627
                throw 'failed to decipher data';
3628
            }
3629
3630
            return plaindata;
3631
        }
3632
3633
        /**
3634
         * decrypt the actual paste text
3635
         *
3636
         * @private
3637
         * @function
3638
         * @param {object} paste - paste data in object form
3639
         * @param {string} key
3640
         * @param {string} password
3641
         * @param {bool} ignoreError - ignore decryption errors iof set to true
3642
         * @return {bool} - whether action was successful
3643
         * @throws {string}
3644
         */
3645
        function decryptPaste(paste, key, password, ignoreError)
3646
        {
3647
            var plaintext
3648
            if (ignoreError === true) {
3649
                plaintext = CryptTool.decipher(key, password, paste.data);
3650
            } else {
3651
                try {
3652
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3653
                } catch (err) {
3654
                    throw 'failed to decipher paste text: ' + err
3655
                }
3656
                if (plaintext === false) {
3657
                    return false;
3658
                }
3659
            }
3660
3661
            // on success show paste
3662
            PasteViewer.setFormat(paste.meta.formatter);
3663
            PasteViewer.setText(plaintext);
3664
            // trigger to show the text (attachment loaded afterwards)
3665
            PasteViewer.run();
3666
3667
            return true;
3668
        }
3669
3670
        /**
3671
         * decrypts any attachment
3672
         *
3673
         * @private
3674
         * @function
3675
         * @param {object} paste - paste data in object form
3676
         * @param {string} key
3677
         * @param {string} password
3678
         * @return {bool} - whether action was successful
3679
         * @throws {string}
3680
         */
3681
        function decryptAttachment(paste, key, password)
3682
        {
3683
            // decrypt attachment
3684
            try {
3685
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3686
            } catch (err) {
3687
                throw 'failed to decipher attachment: ' + err
3688
            }
3689
            if (attachment === false) {
3690
                return false;
3691
            }
3692
3693
            // decrypt attachment name
3694
            var attachmentName;
3695
            if (paste.attachmentname) {
3696
                try {
3697
                    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 3694. 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...
3698
                } catch (err) {
3699
                    throw 'failed to decipher attachment name: ' + err
3700
                }
3701
                if (attachmentName === false) {
3702
                    return false;
3703
                }
3704
            }
3705
3706
            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 3695 is false. Are you sure the function setAttachment handles undefined variables?
Loading history...
3707
            AttachmentViewer.showAttachment();
3708
3709
            return true;
3710
        }
3711
3712
        /**
3713
         * decrypts all comments and shows them
3714
         *
3715
         * @private
3716
         * @function
3717
         * @param {object} paste - paste data in object form
3718
         * @param {string} key
3719
         * @param {string} password
3720
         * @return {bool} - whether action was successful
3721
         */
3722
        function decryptComments(paste, key, password)
3723
        {
3724
            // remove potentially previous discussion
3725
            DiscussionViewer.prepareNewDisucssion();
3726
3727
            // iterate over comments
3728
            for (var i = 0; i < paste.comments.length; ++i) {
3729
                var comment = paste.comments[i];
3730
3731
                DiscussionViewer.addComment(
3732
                    comment,
3733
                    CryptTool.decipher(key, password, comment.data),
3734
                    CryptTool.decipher(key, password, comment.meta.nickname)
3735
                );
3736
            }
3737
3738
            DiscussionViewer.finishDiscussion();
3739
            DiscussionViewer.showDiscussion();
3740
            return true;
3741
        }
3742
3743
        /**
3744
         * show decrypted text in the display area, including discussion (if open)
3745
         *
3746
         * @name   PasteDecrypter.run
3747
         * @function
3748
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3749
         */
3750
        me.run = function(paste)
3751
        {
3752
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3753
3754
            if (typeof paste === 'undefined') {
3755
                paste = $.parseJSON(Model.getCipherData());
3756
            }
3757
3758
            var key = Model.getPasteKey(),
3759
                password = Prompt.getPassword();
3760
3761
            if (PasteViewer.isPrettyPrinted()) {
3762
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3763
                return;
3764
            }
3765
3766
            // try to decrypt the paste
3767
            try {
3768
                Prompt.setPasswordCallback(function () {
3769
                    me.run(paste);
3770
                });
3771
3772
                // decrypt attachments
3773
                if (paste.attachment) {
3774
                    // try to decrypt paste and if it fails (because the password is
3775
                    // missing) return to let JS continue and wait for user
3776
                    if (!decryptAttachment(paste, key, password)) {
3777
                        return;
3778
                    }
3779
                }
3780
3781
                // Deliberately ignores non-critical errors as this decryption
3782
                // can also return an empty string and when this is done, the
3783
                // decryption routine cannot differenciate this to an error.
3784
                // As, however, the attachment could already be decrypted we
3785
                // can continue here without showing an error, but just an empty
3786
                // paste text.
3787
                decryptPaste(paste, key, password, true);
3788
            } catch(err) {
3789
                Alert.hideLoading();
3790
3791
                // log and show error
3792
                console.error(err);
3793
                Alert.showError('Could not decrypt data (Wrong key?)');
3794
3795
                // still go on to potentially show potentially partially decrypted data
3796
            }
3797
3798
            // shows the remaining time (until) deletion
3799
            PasteStatus.showRemainingTime(paste.meta);
3800
3801
            // if the discussion is opened on this paste, display it
3802
            if (paste.meta.opendiscussion) {
3803
                decryptComments(paste, key, password);
3804
            }
3805
3806
            Alert.hideLoading();
3807
            TopNav.showViewButtons();
3808
        }
3809
3810
        /**
3811
         * initialize
3812
         *
3813
         * @name   PasteDecrypter.init
3814
         * @function
3815
         */
3816
        me.init = function()
3817
        {
3818
            // nothing yet
3819
        }
3820
3821
        return me;
3822
    })();
3823
3824
    /**
3825
     * (controller) main PrivateBin logic
3826
     *
3827
     * @param  {object} window
3828
     * @param  {object} document
3829
     * @class
3830
     */
3831
    var Controller = (function (window, document) {
3832
        var me = {};
3833
3834
        /**
3835
         * hides all status messages no matter which module showed them
3836
         *
3837
         * @name   Controller.hideStatusMessages
3838
         * @function
3839
         */
3840
        me.hideStatusMessages = function()
3841
        {
3842
            PasteStatus.hideMessages();
3843
            Alert.hideMessages();
3844
        }
3845
3846
        /**
3847
         * creates a new paste
3848
         *
3849
         * @name   Controller.newPaste
3850
         * @function
3851
         */
3852
        me.newPaste = function()
3853
        {
3854
            // Important: This *must not* run Alert.hideMessages() as previous
3855
            // errors from viewing a paste should be shown.
3856
            TopNav.hideAllButtons();
3857
            Alert.showLoading('Preparing new paste…', 0, 'time');
3858
3859
            PasteStatus.hideMessages();
3860
            PasteViewer.hide();
3861
            Editor.resetInput();
3862
            Editor.show();
3863
            Editor.focusInput();
3864
3865
            TopNav.loadDefaults();
3866
            TopNav.showCreateButtons();
3867
            Alert.hideLoading();
3868
        }
3869
3870
        /**
3871
         * shows the loaded paste
3872
         *
3873
         * @name   Controller.showPaste
3874
         * @function
3875
         */
3876
        me.showPaste = function()
3877
        {
3878
            try {
3879
                Model.getPasteId();
3880
                Model.getPasteKey();
3881
            } catch (err) {
3882
                console.error(err);
3883
3884
                // missing decryption key (or paste ID) in URL?
3885
                if (window.location.hash.length === 0) {
3886
                    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?)');
3887
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3888
                    return;
3889
                }
3890
            }
3891
3892
            // show proper elements on screen
3893
            PasteDecrypter.run();
3894
            return;
0 ignored issues
show
Unused Code introduced by
This return has no effect and can be removed.
Loading history...
3895
        }
3896
3897
        /**
3898
         * refreshes the loaded paste to show potential new data
3899
         *
3900
         * @name   Controller.refreshPaste
3901
         * @function
3902
         * @param {function} callback
3903
         */
3904
        me.refreshPaste = function(callback)
3905
        {
3906
            // save window position to restore it later
3907
            var orgPosition = $(window).scrollTop();
3908
3909
            Uploader.prepare();
3910
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3911
3912
            Uploader.setFailure(function (status, data) {
3913
                // revert loading status…
3914
                Alert.hideLoading();
3915
                TopNav.showViewButtons();
3916
3917
                // show error message
3918
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3919
            })
3920
            Uploader.setSuccess(function (status, data) {
3921
                PasteDecrypter.run(data);
3922
3923
                // restore position
3924
                window.scrollTo(0, orgPosition);
3925
3926
                callback();
3927
            })
3928
            Uploader.run();
3929
        }
3930
3931
        /**
3932
         * clone the current paste
3933
         *
3934
         * @name   Controller.clonePaste
3935
         * @function
3936
         * @param  {Event} event
3937
         */
3938
        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...
3939
        {
3940
            TopNav.collapseBar();
3941
            TopNav.hideAllButtons();
3942
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3943
3944
            // hide messages from previous paste
3945
            me.hideStatusMessages();
3946
3947
            // erase the id and the key in url
3948
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3949
3950
            if (AttachmentViewer.hasAttachment()) {
3951
                AttachmentViewer.moveAttachmentTo(
3952
                    TopNav.getCustomAttachment(),
3953
                    'Cloned: \'%s\''
3954
                );
3955
                TopNav.hideFileSelector();
3956
                AttachmentViewer.hideAttachment();
3957
                // NOTE: it also looks nice without removing the attachment
3958
                // but for a consistent display we remove it…
3959
                AttachmentViewer.hideAttachmentPreview();
3960
                TopNav.showCustomAttachment();
3961
3962
                // show another status message to make the user aware that the
3963
                // file was cloned too!
3964
                Alert.showStatus(
3965
                    [
3966
                        'The cloned file \'%s\' was attached to this paste.',
3967
                        AttachmentViewer.getAttachment()[1]
3968
                    ], 'copy', true, true);
3969
            }
3970
3971
            Editor.setText(PasteViewer.getText())
3972
            PasteViewer.hide();
3973
            Editor.show();
3974
3975
            Alert.hideLoading();
3976
            TopNav.showCreateButtons();
3977
        }
3978
3979
        /**
3980
         * removes a saved paste
3981
         *
3982
         * @name   Controller.removePaste
3983
         * @function
3984
         * @param  {string} pasteId
3985
         * @param  {string} deleteToken
3986
         */
3987
        me.removePaste = function(pasteId, deleteToken) {
3988
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3989
            // so we use a POST request
3990
            Uploader.prepare();
3991
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3992
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3993
3994
            Uploader.setFailure(function () {
3995
                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3996
            })
3997
            Uploader.run();
3998
        }
3999
4000
        /**
4001
         * application start
4002
         *
4003
         * @name   Controller.init
4004
         * @function
4005
         */
4006
        me.init = function()
4007
        {
4008
            // first load translations
4009
            I18n.loadTranslations();
4010
4011
            // initialize other modules/"classes"
4012
            Alert.init();
4013
            Model.init();
4014
4015
            AttachmentViewer.init();
4016
            CryptTool.init();
4017
            DiscussionViewer.init();
4018
            Editor.init();
4019
            PasteDecrypter.init();
4020
            PasteEncrypter.init();
4021
            PasteStatus.init();
4022
            PasteViewer.init();
4023
            Prompt.init();
4024
            TopNav.init();
4025
            UiHelper.init();
4026
            Uploader.init();
4027
4028
            // display an existing paste
4029
            if (Model.hasCipherData()) {
4030
                return me.showPaste();
4031
            }
4032
4033
            // otherwise create a new paste
4034
            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...
4035
        }
4036
4037
        return me;
4038
    })(window, document);
4039
4040
    return {
4041
        Helper: Helper,
4042
        I18n: I18n,
4043
        CryptTool: CryptTool,
4044
        Model: Model,
4045
        UiHelper: UiHelper,
4046
        Alert: Alert,
4047
        PasteStatus: PasteStatus,
4048
        Prompt: Prompt,
4049
        Editor: Editor,
4050
        PasteViewer: PasteViewer,
4051
        AttachmentViewer: AttachmentViewer,
4052
        DiscussionViewer: DiscussionViewer,
4053
        TopNav: TopNav,
4054
        Uploader: Uploader,
4055
        PasteEncrypter: PasteEncrypter,
4056
        PasteDecrypter: PasteDecrypter,
4057
        Controller: Controller
4058
    };
4059
}(jQuery, sjcl, Base64, RawDeflate);
4060