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

AttachmentViewer.constructor   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 161

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 161
rs 8.2857
c 1
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
            // window.baseURI isn't emulated by JSdom
280
            var loc = window.location;
281
            baseUri = loc.href.substring(
282
                0,
283
                loc.href.length - loc.search.length - loc.hash.length
284
            );
285
286
            // if base uri contains query string (when no base tag is present),
287
            // it is unwanted
288
            var queryIndex = baseUri.indexOf('?');
289
            if (queryIndex !== -1) {
290
                // so we built our own baseuri
291
                baseUri = baseUri.substring(0, queryIndex);
292
            }
293
294
            return baseUri;
295
        }
296
297
        /**
298
         * convert all applicable characters to HTML entities
299
         *
300
         * @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}
301
         * @name   Helper.htmlEntities
302
         * @function
303
         * @param  {string} str
304
         * @return {string} escaped HTML
305
         */
306
        me.htmlEntities = function(str) {
307
            return String(str).replace(
308
                /[&<>"'`=\/]/g, function(s) {
309
                    return entityMap[s];
310
                });
311
        }
312
313
        /**
314
         * resets state, used for unit testing
315
         *
316
         * @name   Model.reset
317
         * @function
318
         */
319
        me.reset = function()
320
        {
321
            baseUri = null;
322
        }
323
324
        return me;
325
    })();
326
327
    /**
328
     * internationalization module
329
     *
330
     * @param  {object} window
331
     * @param  {object} document
332
     * @class
333
     */
334
    var I18n = (function (window, document) {
335
        var me = {};
336
337
        /**
338
         * const for string of loaded language
339
         *
340
         * @private
341
         * @prop   {string}
342
         * @readonly
343
         */
344
        var languageLoadedEvent = 'languageLoaded';
345
346
        /**
347
         * supported languages, minus the built in 'en'
348
         *
349
         * @private
350
         * @prop   {string[]}
351
         * @readonly
352
         */
353
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
354
355
        /**
356
         * built in language
357
         *
358
         * @private
359
         * @prop   {string|null}
360
         */
361
        var language = null;
362
363
        /**
364
         * translation cache
365
         *
366
         * @private
367
         * @enum   {Object}
368
         */
369
        var translations = {};
370
371
        /**
372
         * translate a string, alias for I18n.translate()
373
         *
374
         * for a full description see me.translate
375
         *
376
         * @name   I18n._
377
         * @function
378
         * @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...
379
         * @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...
380
         * @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...
381
         * @return {string}
382
         */
383
        me._ = function()
384
        {
385
            return me.translate.apply(this, arguments);
386
        }
387
388
        /**
389
         * translate a string
390
         *
391
         * Optionally pass a jQuery element as the first parameter, to automatically
392
         * let the text of this element be replaced. In case the (asynchronously
393
         * loaded) language is not downloadet yet, this will make sure the string
394
         * is replaced when it is actually loaded.
395
         * So for easy translations passing the jQuery object to apply it to is
396
         * more save, especially when they are loaded in the beginning.
397
         *
398
         * @name   I18n.translate
399
         * @function
400
         * @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...
401
         * @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...
402
         * @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...
403
         * @return {string}
404
         */
405
        me.translate = function()
406
        {
407
            // convert parameters to array
408
            var args = Array.prototype.slice.call(arguments),
409
                messageId,
410
                $element = null;
411
412
            // parse arguments
413
            if (args[0] instanceof jQuery) {
414
                // optional jQuery element as first parameter
415
                $element = args[0];
416
                args.shift();
417
            }
418
419
            // extract messageId from arguments
420
            var usesPlurals = $.isArray(args[0]);
421
            if (usesPlurals) {
422
                // use the first plural form as messageId, otherwise the singular
423
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
424
            } else {
425
                messageId = args[0];
426
            }
427
428
            if (messageId.length === 0) {
429
                return messageId;
430
            }
431
432
            // if no translation string cannot be found (in translations object)
433
            if (!translations.hasOwnProperty(messageId)) {
434
                // if language is still loading and we have an elemt assigned
435
                if (language === null && $element !== null) {
436
                    // handle the error by attaching the language loaded event
437
                    var orgArguments = arguments;
438
                    $(document).on(languageLoadedEvent, function () {
439
                        // log to show that the previous error could be mitigated
440
                        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...
441
                        // re-execute this function
442
                        me.translate.apply(this, orgArguments);
443
                    });
444
445
                    // and fall back to English for now until the real language
446
                    // file is loaded
447
                }
448
449
                // for all other langauges than English for which thsi behaviour
450
                // is expected as it is built-in, log error
451
                if (language !== 'en') {
452
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
453
                    // fallback to English
454
                }
455
456
                // save English translation (should be the same on both sides)
457
                translations[messageId] = args[0];
458
            }
459
460
            // lookup plural translation
461
            if (usesPlurals && $.isArray(translations[messageId])) {
462
                var n = parseInt(args[1] || 1, 10),
463
                    key = me.getPluralForm(n),
464
                    maxKey = translations[messageId].length - 1;
465
                if (key > maxKey) {
466
                    key = maxKey;
467
                }
468
                args[0] = translations[messageId][key];
469
                args[1] = n;
470
            } else {
471
                // lookup singular translation
472
                args[0] = translations[messageId];
473
            }
474
475
            // format string
476
            var output = Helper.sprintf.apply(this, args);
477
478
            // if $element is given, apply text to element
479
            if ($element !== null) {
480
                $element.text(output);
481
            }
482
483
            return output;
484
        }
485
486
        /**
487
         * per language functions to use to determine the plural form
488
         *
489
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
490
         * @name   I18n.getPluralForm
491
         * @function
492
         * @param  {int} n
493
         * @return {int} array key
494
         */
495
        me.getPluralForm = function(n) {
496
            switch (language)
497
            {
498
                case 'fr':
499
                case 'oc':
500
                case 'zh':
501
                    return (n > 1 ? 1 : 0);
502
                case 'pl':
503
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
504
                case 'ru':
505
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
506
                case 'sl':
507
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
508
                // de, en, es, it, no, pt
509
                default:
510
                    return (n !== 1 ? 1 : 0);
511
            }
512
        }
513
514
        /**
515
         * load translations into cache
516
         *
517
         * @name   I18n.loadTranslations
518
         * @function
519
         */
520
        me.loadTranslations = function()
521
        {
522
            var newLanguage = Helper.getCookie('lang');
523
524
            // auto-select language based on browser settings
525
            if (newLanguage.length === 0) {
526
                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
527
            }
528
529
            // if language is already used skip update
530
            if (newLanguage === language) {
531
                return;
532
            }
533
534
            // if language is built-in (English) skip update
535
            if (newLanguage === 'en') {
536
                language = 'en';
537
                return;
538
            }
539
540
            // if language is not supported, show error
541
            if (supportedLanguages.indexOf(newLanguage) === -1) {
542
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
543
                language = 'en';
544
                return;
545
            }
546
547
            // load strings from JSON
548
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
549
                language = newLanguage;
550
                translations = data;
551
                $(document).triggerHandler(languageLoadedEvent);
552
            }).fail(function (data, textStatus, errorMsg) {
553
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
554
                language = 'en';
555
            });
556
        }
557
558
        return me;
559
    })(window, document);
560
561
    /**
562
     * handles everything related to en/decryption
563
     *
564
     * @class
565
     */
566
    var CryptTool = (function () {
567
        var me = {};
568
569
        /**
570
         * compress a message (deflate compression), returns base64 encoded data
571
         *
572
         * @name   cryptToolcompress
573
         * @function
574
         * @private
575
         * @param  {string} message
576
         * @return {string} base64 data
577
         */
578
        function compress(message)
579
        {
580
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
581
        }
582
583
        /**
584
         * decompress a message compressed with cryptToolcompress()
585
         *
586
         * @name   cryptTooldecompress
587
         * @function
588
         * @private
589
         * @param  {string} data - base64 data
590
         * @return {string} message
591
         */
592
        function decompress(data)
593
        {
594
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
595
        }
596
597
        /**
598
         * compress, then encrypt message with given key and password
599
         *
600
         * @name   CryptTool.cipher
601
         * @function
602
         * @param  {string} key
603
         * @param  {string} password
604
         * @param  {string} message
605
         * @return {string} data - JSON with encrypted data
606
         */
607
        me.cipher = function(key, password, message)
608
        {
609
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
610
            var options = {
611
                mode: 'gcm',
612
                ks: 256,
613
                ts: 128
614
            };
615
616
            if ((password || '').trim().length === 0) {
617
                return sjcl.encrypt(key, compress(message), options);
618
            }
619
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
620
        }
621
622
        /**
623
         * decrypt message with key, then decompress
624
         *
625
         * @name   CryptTool.decipher
626
         * @function
627
         * @param  {string} key
628
         * @param  {string} password
629
         * @param  {string} data - JSON with encrypted data
630
         * @return {string} decrypted message
631
         */
632
        me.decipher = function(key, password, data)
633
        {
634
            if (data !== undefined) {
635
                try {
636
                    return decompress(sjcl.decrypt(key, data));
637
                } catch(err) {
638
                    try {
639
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
640
                    } catch(e) {
641
                        // ignore error, because ????? @TODO
642
                    }
643
                }
644
            }
645
            return '';
646
        }
647
648
        /**
649
         * checks whether the crypt tool is ready.
650
         *
651
         * @name   CryptTool.isReady
652
         * @function
653
         * @return {bool}
654
         */
655
        me.isEntropyReady = function()
656
        {
657
            return sjcl.random.isReady();
658
        }
659
660
        /**
661
         * checks whether the crypt tool is ready.
662
         *
663
         * @name   CryptTool.isReady
664
         * @function
665
         * @param {function} func
666
         */
667
        me.addEntropySeedListener = function(func)
668
        {
669
            sjcl.random.addEventListener('seeded', func);
670
        }
671
672
        /**
673
         * returns a random symmetric key
674
         *
675
         * @name   CryptTool.getSymmetricKey
676
         * @function
677
         * @return {string} func
678
         */
679
        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...
680
        {
681
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
682
        }
683
684
        /**
685
         * initialize crypt tool
686
         *
687
         * @name   CryptTool.init
688
         * @function
689
         */
690
        me.init = function()
691
        {
692
            // will fail earlier as sjcl is already passed as a parameter
693
            // if (typeof sjcl !== 'object') {
694
            //     Alert.showError(
695
            //         I18n._('The library %s is not available.', 'sjcl') +
696
            //         I18n._('Messages cannot be decrypted or encrypted.')
697
            //     );
698
            // }
699
        }
700
701
        return me;
702
    })();
703
704
    /**
705
     * (Model) Data source (aka MVC)
706
     *
707
     * @class
708
     */
709
    var Model = (function () {
710
        var me = {};
711
712
        var $cipherData,
713
            $templates;
714
715
        var id = null, symmetricKey = null;
716
717
        /**
718
         * returns the expiration set in the HTML
719
         *
720
         * @name   Model.getExpirationDefault
721
         * @function
722
         * @return string
723
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
724
         */
725
        me.getExpirationDefault = function()
726
        {
727
            return $('#pasteExpiration').val();
728
        }
729
730
        /**
731
         * returns the format set in the HTML
732
         *
733
         * @name   Model.getFormatDefault
734
         * @function
735
         * @return string
736
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
737
         */
738
        me.getFormatDefault = function()
739
        {
740
            return $('#pasteFormatter').val();
741
        }
742
743
        /**
744
         * check if cipher data was supplied
745
         *
746
         * @name   Model.getCipherData
747
         * @function
748
         * @return boolean
749
         */
750
        me.hasCipherData = function()
751
        {
752
            return (me.getCipherData().length > 0);
753
        }
754
755
        /**
756
         * returns the cipher data
757
         *
758
         * @name   Model.getCipherData
759
         * @function
760
         * @return string
761
         */
762
        me.getCipherData = function()
763
        {
764
            return $cipherData.text();
765
        }
766
767
        /**
768
         * get the pastes unique identifier from the URL,
769
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
770
         *
771
         * @name   Model.getPasteId
772
         * @function
773
         * @return {string} unique identifier
774
         * @throws {string}
775
         */
776
        me.getPasteId = function()
777
        {
778
            if (id === null) {
779
                id = window.location.search.substring(1);
780
781
                if (id === '') {
782
                    throw 'no paste id given';
783
                }
784
            }
785
786
            return id;
787
        }
788
789
        /**
790
         * return the deciphering key stored in anchor part of the URL
791
         *
792
         * @name   Model.getPasteKey
793
         * @function
794
         * @return {string|null} key
795
         * @throws {string}
796
         */
797
        me.getPasteKey = function()
798
        {
799
            if (symmetricKey === null) {
800
                symmetricKey = window.location.hash.substring(1);
801
802
                if (symmetricKey === '') {
803
                    throw 'no encryption key given';
804
                }
805
806
                // Some web 2.0 services and redirectors add data AFTER the anchor
807
                // (such as &utm_source=...). We will strip any additional data.
808
                var ampersandPos = symmetricKey.indexOf('&');
809
                if (ampersandPos > -1)
810
                {
811
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
812
                }
813
            }
814
815
            return symmetricKey;
816
        }
817
818
        /**
819
         * returns a jQuery copy of the HTML template
820
         *
821
         * @name Model.getTemplate
822
         * @function
823
         * @param  {string} name - the name of the template
824
         * @return {jQuery}
825
         */
826
        me.getTemplate = function(name)
827
        {
828
            // find template
829
            var $element = $templates.find('#' + name + 'template').clone(true);
830
            // change ID to avoid collisions (one ID should really be unique)
831
            return $element.prop('id', name);
832
        }
833
834
        /**
835
         * resets state, used for unit testing
836
         *
837
         * @name   Model.reset
838
         * @function
839
         */
840
        me.reset = function()
841
        {
842
            $cipherData = $templates = id = symmetricKey = null;
843
        }
844
845
846
        /**
847
         * init navigation manager
848
         *
849
         * preloads jQuery elements
850
         *
851
         * @name   Model.init
852
         * @function
853
         */
854
        me.init = function()
855
        {
856
            $cipherData = $('#cipherdata');
857
            $templates = $('#templates');
858
        }
859
860
        return me;
861
    })();
862
863
    /**
864
     * Helper functions for user interface
865
     *
866
     * everything directly UI-related, which fits nowhere else
867
     *
868
     * @param  {object} window
869
     * @param  {object} document
870
     * @class
871
     */
872
    var UiHelper = (function (window, document) {
873
        var me = {};
874
875
        /**
876
         * handle history (pop) state changes
877
         *
878
         * currently this does only handle redirects to the home page.
879
         *
880
         * @private
881
         * @function
882
         * @param  {Event} event
883
         */
884
        function historyChange(event)
885
        {
886
            var currentLocation = Helper.baseUri();
887
            if (event.originalEvent.state === null && // no state object passed
888
                event.originalEvent.target.location.href === currentLocation && // target location is home page
889
                window.location.href === currentLocation // and we are not already on the home page
890
            ) {
891
                // redirect to home page
892
                window.location.href = currentLocation;
893
            }
894
        }
895
896
        /**
897
         * reload the page
898
         *
899
         * This takes the user to the PrivateBin homepage.
900
         *
901
         * @name   UiHelper.reloadHome
902
         * @function
903
         */
904
        me.reloadHome = function()
905
        {
906
            window.location.href = Helper.baseUri();
907
        }
908
909
        /**
910
         * checks whether the element is currently visible in the viewport (so
911
         * the user can actually see it)
912
         *
913
         * THanks to https://stackoverflow.com/a/40658647
914
         *
915
         * @name   UiHelper.isVisible
916
         * @function
917
         * @param  {jQuery} $element The link hash to move to.
918
         */
919
        me.isVisible = function($element)
920
        {
921
            var elementTop = $element.offset().top;
922
            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...
923
924
            var viewportTop = $(window).scrollTop();
925
            var viewportBottom = viewportTop + $(window).height();
926
927
            return (elementTop > viewportTop && elementTop < viewportBottom);
928
        }
929
930
        /**
931
         * scrolls to a specific element
932
         *
933
         * Based on code by @hanoo: https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767
934
         *
935
         * @name   UiHelper.scrollTo
936
         * @param  {jQuery}           $element        The link hash to move to.
937
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
938
         * @param  {string}           animationEffect   passed to jQuery .animate
939
         * @param  {function}         finishedCallback  function to call after animation finished
940
         */
941
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
942
        {
943
            var $body = $('html, body'),
944
                margin = 50,
945
                callbackCalled = false;
946
947
            //calculate destination place
948
            var dest = 0;
949
            // if it would scroll out of the screen at the bottom only scroll it as
950
            // far as the screen can go
951
            if ($element.offset().top > $(document).height() - $(window).height()) {
952
                dest = $(document).height() - $(window).height();
953
            } else {
954
                dest = $element.offset().top - margin;
955
            }
956
            // skip animation if duration is set to 0
957
            if (animationDuration === 0) {
958
                window.scrollTo(0, dest);
959
            } else {
960
                // stop previous animation
961
                $body.stop();
962
                // scroll to destination
963
                $body.animate({
964
                    scrollTop: dest
965
                }, animationDuration, animationEffect);
966
            }
967
968
            // as we have finished we can enable scrolling again
969
            $body.queue(function (next) {
970
                if (!callbackCalled) {
971
                    // call user function if needed
972
                    if (typeof finishedCallback !== 'undefined') {
973
                        finishedCallback();
974
                    }
975
976
                    // prevent calling this function twice
977
                    callbackCalled = true;
978
                }
979
                next();
980
            });
981
        }
982
983
        /**
984
         * initialize
985
         *
986
         * @name   UiHelper.init
987
         * @function
988
         */
989
        me.init = function()
990
        {
991
            // update link to home page
992
            $('.reloadlink').prop('href', Helper.baseUri());
993
994
            $(window).on('popstate', historyChange);
995
        }
996
997
        return me;
998
    })(window, document);
999
1000
    /**
1001
     * Alert/error manager
1002
     *
1003
     * @param  {object} window
1004
     * @param  {object} document
1005
     * @class
1006
     */
1007
    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...
1008
        var me = {};
1009
1010
        var $errorMessage,
1011
            $loadingIndicator,
1012
            $statusMessage;
1013
1014
        var currentIcon = [
1015
            'glyphicon-time', // loading icon
1016
            'glyphicon-info-sign', // status icon
1017
            '', // resevered for warning, not used yet
1018
            'glyphicon-alert' // error icon
1019
        ];
1020
1021
        var alertType = [
1022
            'loading', // not in bootstrap, but using a good value here
1023
            'info', // status icon
1024
            'warning', // not used yet
1025
            'danger' // error icon
1026
        ];
1027
1028
        var customHandler;
1029
1030
        /**
1031
         * forwards a request to the i18n module and shows the element
1032
         *
1033
         * @private
1034
         * @function
1035
         * @param  {int} id - id of notification
1036
         * @param  {jQuery} $element - jQuery object
1037
         * @param  {string|array} args
1038
         * @param  {string|null} icon - optional, icon
1039
         */
1040
        function handleNotification(id, $element, args, icon)
1041
        {
1042
            // basic parsing/conversion of parameters
1043
            if (typeof icon === 'undefined') {
1044
                icon = null;
1045
            }
1046
            if (typeof args === 'undefined') {
1047
                args = null;
1048
            } else if (typeof args === 'string') {
1049
                // convert string to array if needed
1050
                args = [args];
1051
            }
1052
1053
            // pass to custom handler if dfined
1054
            if (typeof customHandler === 'function') {
1055
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1056
                if (handlerResult === true) {
1057
                    // if it returs true, skip own handler
1058
                    return;
1059
                }
1060
                if (handlerResult instanceof jQuery) {
1061
                    // continue processing with new element
1062
                    $element = handlerResult;
1063
                    icon = null; // icons not supported in this case
1064
                }
1065
            }
1066
1067
            // handle icon
1068
            if (icon !== null && // icon was passed
1069
                icon !== currentIcon[id] // and it differs from current icon
1070
            ) {
1071
                var $glyphIcon = $element.find(':first');
1072
1073
                // remove (previous) icon
1074
                $glyphIcon.removeClass(currentIcon[id]);
1075
1076
                // any other thing as a string (e.g. 'null') (only) removes the icon
1077
                if (typeof icon === 'string') {
1078
                    // set new icon
1079
                    currentIcon[id] = 'glyphicon-' + icon;
1080
                    $glyphIcon.addClass(currentIcon[id]);
1081
                }
1082
            }
1083
1084
            // show text
1085
            if (args !== null) {
1086
                // add jQuery object to it as first parameter
1087
                args.unshift($element.find(':last'));
1088
1089
                // pass it to I18n
1090
                I18n._.apply(this, args);
1091
            }
1092
1093
            // show notification
1094
            $element.removeClass('hidden');
1095
        }
1096
1097
        /**
1098
         * display a status message
1099
         *
1100
         * This automatically passes the text to I18n for translation.
1101
         *
1102
         * @name   Alert.showStatus
1103
         * @function
1104
         * @param  {string|array} message     string, use an array for %s/%d options
1105
         * @param  {string|null}  icon        optional, the icon to show,
1106
         *                                    default: leave previous icon
1107
         * @param  {bool}         dismissable optional, whether the notification
1108
         *                                    can be dismissed (closed), default: false
1109
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1110
         *                                    notification should be hidden automatically;
1111
         *                                    default: disabled (0); use true for default value
1112
         */
1113
        me.showStatus = function(message, icon, dismissable, autoclose)
0 ignored issues
show
Unused Code introduced by
The parameter dismissable is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter autoclose is not used and could be removed.

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

Loading history...
1114
        {
1115
            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...
1116
            // @TODO: implement dismissable
1117
            // @TODO: implement autoclose
1118
1119
            handleNotification(1, $statusMessage, message, icon);
1120
        }
1121
1122
        /**
1123
         * display an error message
1124
         *
1125
         * This automatically passes the text to I18n for translation.
1126
         *
1127
         * @name   Alert.showError
1128
         * @function
1129
         * @param  {string|array} message     string, use an array for %s/%d options
1130
         * @param  {string|null}  icon        optional, the icon to show, default:
1131
         *                                    leave previous icon
1132
         * @param  {bool}         dismissable optional, whether the notification
1133
         *                                    can be dismissed (closed), default: false
1134
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1135
         *                                    notification should be hidden automatically;
1136
         *                                    default: disabled (0); use true for default value
1137
         */
1138
        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...
1139
        {
1140
            console.error('error message shown: ', message);
1141
            // @TODO: implement dismissable (bootstrap add-on has it)
1142
            // @TODO: implement autoclose
1143
1144
            handleNotification(3, $errorMessage, message, icon);
1145
        }
1146
1147
        /**
1148
         * shows a loading message, optionally with a percentage
1149
         *
1150
         * This automatically passes all texts to the i10s module.
1151
         *
1152
         * @name   Alert.showLoading
1153
         * @function
1154
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1155
         * @param  {int}               percentage   optional, default: null
1156
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1157
         */
1158
        me.showLoading = function(message, percentage, icon)
1159
        {
1160
            if (typeof message !== 'undefined' && message !== null) {
1161
                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...
1162
            }
1163
1164
            // default message text
1165
            if (typeof message === 'undefined') {
1166
                message = 'Loading…';
1167
            }
1168
1169
            // currently percentage parameter is ignored
1170
            // // @TODO handle it here…
1171
1172
            handleNotification(0, $loadingIndicator, message, icon);
1173
1174
            // show loading status (cursor)
1175
            $('body').addClass('loading');
1176
        }
1177
1178
        /**
1179
         * hides the loading message
1180
         *
1181
         * @name   Alert.hideLoading
1182
         * @function
1183
         */
1184
        me.hideLoading = function()
1185
        {
1186
            $loadingIndicator.addClass('hidden');
1187
1188
            // hide loading cursor
1189
            $('body').removeClass('loading');
1190
        }
1191
1192
        /**
1193
         * hides any status/error messages
1194
         *
1195
         * This does not include the loading message.
1196
         *
1197
         * @name   Alert.hideMessages
1198
         * @function
1199
         */
1200
        me.hideMessages = function()
1201
        {
1202
            // also possible: $('.statusmessage').addClass('hidden');
1203
            $statusMessage.addClass('hidden');
1204
            $errorMessage.addClass('hidden');
1205
        }
1206
1207
        /**
1208
         * set a custom handler, which gets all notifications.
1209
         *
1210
         * This handler gets the following arguments:
1211
         * alertType (see array), $element, args, icon
1212
         * If it returns true, the own processing will be stopped so the message
1213
         * will not be displayed. Otherwise it will continue.
1214
         * As an aditional feature it can return q jQuery element, which will
1215
         * then be used to add the message there. Icons are not supported in
1216
         * that case and will be ignored.
1217
         * Pass 'null' to reset/delete the custom handler.
1218
         * Note that there is no notification when a message is supposed to get
1219
         * hidden.
1220
         *
1221
         * @name   Alert.setCustomHandler
1222
         * @function
1223
         * @param {function|null} newHandler
1224
         */
1225
        me.setCustomHandler = function(newHandler)
1226
        {
1227
            customHandler = newHandler;
1228
        }
1229
1230
        /**
1231
         * init status manager
1232
         *
1233
         * preloads jQuery elements
1234
         *
1235
         * @name   Alert.init
1236
         * @function
1237
         */
1238
        me.init = function()
1239
        {
1240
            // hide "no javascript" error message
1241
            $('#noscript').hide();
1242
1243
            // not a reset, but first set of the elements
1244
            $errorMessage = $('#errormessage');
1245
            $loadingIndicator = $('#loadingindicator');
1246
            $statusMessage = $('#status');
1247
1248
            // display status returned by php code, if any (e.g. paste was properly deleted)
1249
            var serverStatus = $statusMessage.text();
1250
            if (Helper.isValidText(serverStatus)) {
1251
                me.showStatus();
1252
            }
1253
1254
            // display error message from php code
1255
            var serverError = $errorMessage.text();
1256
            if (Helper.isValidText(serverError)) {
1257
                Alert.showError();
1258
            }
1259
        }
1260
1261
        return me;
1262
    })(window, document);
1263
1264
    /**
1265
     * handles paste status/result
1266
     *
1267
     * @param  {object} window
1268
     * @param  {object} document
1269
     * @class
1270
     */
1271
    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...
1272
        var me = {};
1273
1274
        var $pasteSuccess,
1275
            $pasteUrl,
1276
            $remainingTime,
1277
            $shortenButton;
1278
1279
        /**
1280
         * forward to URL shortener
1281
         *
1282
         * @private
1283
         * @function
1284
         * @param  {Event} event
1285
         */
1286
        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...
1287
        {
1288
            window.location.href = $shortenButton.data('shortener')
1289
                                   + encodeURIComponent($pasteUrl.attr('href'));
1290
        }
1291
1292
        /**
1293
         * Forces opening the paste if the link does not do this automatically.
1294
         *
1295
         * This is necessary as browsers will not reload the page when it is
1296
         * already loaded (which is fake as it is set via history.pushState()).
1297
         *
1298
         * @name   Controller.pasteLinkClick
1299
         * @function
1300
         * @param  {Event} event
1301
         */
1302
        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...
1303
        {
1304
            // check if location is (already) shown in URL bar
1305
            if (window.location.href === $pasteUrl.attr('href')) {
1306
                // if so we need to load link by reloading the current site
1307
                window.location.reload(true);
1308
            }
1309
        }
1310
1311
        /**
1312
         * creates a notification after a successfull paste upload
1313
         *
1314
         * @name   PasteStatus.createPasteNotification
1315
         * @function
1316
         * @param  {string} url
1317
         * @param  {string} deleteUrl
1318
         */
1319
        me.createPasteNotification = function(url, deleteUrl)
1320
        {
1321
            $('#pastelink').html(
1322
                I18n._(
1323
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1324
                    url, url
1325
                )
1326
            );
1327
            // save newly created element
1328
            $pasteUrl = $('#pasteurl');
1329
            // and add click event
1330
            $pasteUrl.click(pasteLinkClick);
1331
1332
            // shorten button
1333
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1334
1335
            // show result
1336
            $pasteSuccess.removeClass('hidden');
1337
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1338
            Helper.selectText($pasteUrl[0]);
1339
        }
1340
1341
        /**
1342
         * shows the remaining time
1343
         *
1344
         * @name PasteStatus.showRemainingTime
1345
         * @function
1346
         * @param {object} pasteMetaData
1347
         */
1348
        me.showRemainingTime = function(pasteMetaData)
1349
        {
1350
            if (pasteMetaData.burnafterreading) {
1351
                // display paste "for your eyes only" if it is deleted
1352
1353
                // actually remove paste, before we claim it is deleted
1354
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1355
1356
                I18n._($remainingTime.find(':last'), "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1357
                $remainingTime.addClass('foryoureyesonly');
1358
1359
                // discourage cloning (it cannot really be prevented)
1360
                TopNav.hideCloneButton();
1361
1362
            } else if (pasteMetaData.expire_date) {
1363
                // display paste expiration
1364
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1365
                    expirationLabel = [
1366
                        'This document will expire in %d ' + expiration[1] + '.',
1367
                        'This document will expire in %d ' + expiration[1] + 's.'
1368
                    ];
1369
1370
                I18n._($remainingTime.find(':last'), expirationLabel, expiration[0]);
1371
                $remainingTime.removeClass('foryoureyesonly')
1372
            } else {
1373
                // never expires
1374
                return;
1375
            }
1376
1377
            // in the end, display notification
1378
            $remainingTime.removeClass('hidden');
1379
        }
1380
1381
        /**
1382
         * hides the remaining time and successful upload notification
1383
         *
1384
         * @name PasteStatus.hideRemainingTime
1385
         * @function
1386
         */
1387
        me.hideMessages = function()
1388
        {
1389
            $remainingTime.addClass('hidden');
1390
            $pasteSuccess.addClass('hidden');
1391
        }
1392
1393
        /**
1394
         * init status manager
1395
         *
1396
         * preloads jQuery elements
1397
         *
1398
         * @name   Alert.init
1399
         * @function
1400
         */
1401
        me.init = function()
1402
        {
1403
            $pasteSuccess = $('#pasteSuccess');
1404
            // $pasteUrl is saved in me.createPasteNotification() after creation
1405
            $remainingTime = $('#remainingtime');
1406
            $shortenButton = $('#shortenbutton');
1407
1408
            // bind elements
1409
            $shortenButton.click(sendToShortener);
1410
        }
1411
1412
        return me;
1413
    })(window, document);
1414
1415
    /**
1416
     * password prompt
1417
     *
1418
     * @param  {object} window
1419
     * @param  {object} document
1420
     * @class
1421
     */
1422
    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...
1423
        var me = {};
1424
1425
        var $passwordDecrypt,
1426
            $passwordForm,
1427
            $passwordModal;
1428
1429
        var password = '',
1430
            passwordCallback = null;
1431
1432
        /**
1433
         * ask the user for the password and set it
1434
         *
1435
         * the callback set via setPasswordCallback is executed
1436
         *
1437
         * @name Prompt.requestPassword()
1438
         * @function
1439
         */
1440
        me.requestPassword = function()
1441
        {
1442
            // show new bootstrap method (if available)
1443
            if ($passwordModal.length !== 0) {
1444
                $passwordModal.modal({
1445
                    backdrop: 'static',
1446
                    keyboard: false
1447
                });
1448
1449
                return;
1450
            }
1451
1452
            // fallback to old method for page template
1453
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1454
            if (newPassword === null) {
1455
                throw 'password prompt canceled';
1456
            }
1457
            if (password.length === 0) {
1458
                // recursive…
1459
                return me.requestPassword();
1460
            }
1461
1462
            password = newPassword;
1463
1464
            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...
1465
                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...
1466
            }
1467
        }
1468
1469
        /**
1470
         * getthe cached password
1471
         *
1472
         * If you do not get a password with this function
1473
         * (returns an empty string), use requestPassword.
1474
         *
1475
         * @name   Prompt.getPassword
1476
         * @function
1477
         * @return {string}
1478
         */
1479
        me.getPassword = function()
1480
        {
1481
            return password;
1482
        }
1483
1484
        /**
1485
         * setsthe callback called when password is entered
1486
         *
1487
         * @name   Prompt.setPasswordCallback
1488
         * @function
1489
         * @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...
1490
         */
1491
        me.setPasswordCallback = function(callback)
1492
        {
1493
            passwordCallback = callback;
1494
        }
1495
1496
        /**
1497
         * submit a password in the modal dialog
1498
         *
1499
         * @private
1500
         * @function
1501
         * @param  {Event} event
1502
         */
1503
        function submitPasswordModal(event)
1504
        {
1505
            // get input
1506
            password = $passwordDecrypt.val();
1507
1508
            // hide modal
1509
            $passwordModal.modal('hide');
1510
1511
            if (passwordCallback !== null) {
1512
                passwordCallback();
1513
            }
1514
1515
            event.preventDefault();
1516
        }
1517
1518
1519
        /**
1520
         * init status manager
1521
         *
1522
         * preloads jQuery elements
1523
         *
1524
         * @name   Controller.init
1525
         * @function
1526
         */
1527
        me.init = function()
1528
        {
1529
            $passwordDecrypt = $('#passworddecrypt');
1530
            $passwordForm = $('#passwordform');
1531
            $passwordModal = $('#passwordmodal');
1532
1533
            // bind events
1534
1535
            // focus password input when it is shown
1536
            $passwordModal.on('shown.bs.Model', function () {
1537
                $passwordDecrypt.focus();
1538
            });
1539
            // handle Model password submission
1540
            $passwordForm.submit(submitPasswordModal);
1541
        }
1542
1543
        return me;
1544
    })(window, document);
1545
1546
    /**
1547
     * Manage paste/message input, and preview tab
1548
     *
1549
     * Note that the actual preview is handled by PasteViewer.
1550
     *
1551
     * @param  {object} window
1552
     * @param  {object} document
1553
     * @class
1554
     */
1555
    var Editor = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter document is not used and could be removed.

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

Loading history...
1556
        var me = {};
1557
1558
        var $editorTabs,
1559
            $messageEdit,
1560
            $messagePreview,
1561
            $message;
1562
1563
        var isPreview = false;
1564
1565
        /**
1566
         * support input of tab character
1567
         *
1568
         * @name   Editor.supportTabs
1569
         * @function
1570
         * @param  {Event} event
1571
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1572
         */
1573
        function supportTabs(event)
1574
        {
1575
            var keyCode = event.keyCode || event.which;
1576
            // tab was pressed
1577
            if (keyCode === 9) {
1578
                // get caret position & selection
1579
                var val   = this.value,
1580
                    start = this.selectionStart,
1581
                    end   = this.selectionEnd;
1582
                // set textarea value to: text before caret + tab + text after caret
1583
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1584
                // put caret at right position again
1585
                this.selectionStart = this.selectionEnd = start + 1;
1586
                // prevent the textarea to lose focus
1587
                event.preventDefault();
1588
            }
1589
        }
1590
1591
        /**
1592
         * view the Editor tab
1593
         *
1594
         * @name   Editor.viewEditor
1595
         * @function
1596
         * @param  {Event} event - optional
1597
         */
1598
        function viewEditor(event)
1599
        {
1600
            // toggle buttons
1601
            $messageEdit.addClass('active');
1602
            $messagePreview.removeClass('active');
1603
1604
            PasteViewer.hide();
1605
1606
            // reshow input
1607
            $message.removeClass('hidden');
1608
1609
            me.focusInput();
1610
1611
            // finish
1612
            isPreview = false;
1613
1614
            // prevent jumping of page to top
1615
            if (typeof event !== 'undefined') {
1616
                event.preventDefault();
1617
            }
1618
        }
1619
1620
        /**
1621
         * view the preview tab
1622
         *
1623
         * @name   Editor.viewPreview
1624
         * @function
1625
         * @param  {Event} event
1626
         */
1627
        function viewPreview(event)
1628
        {
1629
            // toggle buttons
1630
            $messageEdit.removeClass('active');
1631
            $messagePreview.addClass('active');
1632
1633
            // hide input as now preview is shown
1634
            $message.addClass('hidden');
1635
1636
            // show preview
1637
            $('#errormessage').find(':last')
1638
            PasteViewer.setText($message.val());
1639
            PasteViewer.run();
1640
1641
            // finish
1642
            isPreview = true;
1643
1644
            // prevent jumping of page to top
1645
            if (typeof event !== 'undefined') {
1646
                event.preventDefault();
1647
            }
1648
        }
1649
1650
        /**
1651
         * get the state of the preview
1652
         *
1653
         * @name   Editor.isPreview
1654
         * @function
1655
         */
1656
        me.isPreview = function()
1657
        {
1658
            return isPreview;
1659
        }
1660
1661
        /**
1662
         * reset the Editor view
1663
         *
1664
         * @name   Editor.resetInput
1665
         * @function
1666
         */
1667
        me.resetInput = function()
1668
        {
1669
            // go back to input
1670
            if (isPreview) {
1671
                viewEditor();
1672
            }
1673
1674
            // clear content
1675
            $message.val('');
1676
        }
1677
1678
        /**
1679
         * shows the Editor
1680
         *
1681
         * @name   Editor.show
1682
         * @function
1683
         */
1684
        me.show = function()
1685
        {
1686
            $message.removeClass('hidden');
1687
            $editorTabs.removeClass('hidden');
1688
        }
1689
1690
        /**
1691
         * hides the Editor
1692
         *
1693
         * @name   Editor.reset
1694
         * @function
1695
         */
1696
        me.hide = function()
1697
        {
1698
            $message.addClass('hidden');
1699
            $editorTabs.addClass('hidden');
1700
        }
1701
1702
        /**
1703
         * focuses the message input
1704
         *
1705
         * @name   Editor.focusInput
1706
         * @function
1707
         */
1708
        me.focusInput = function()
1709
        {
1710
            $message.focus();
1711
        }
1712
1713
        /**
1714
         * sets a new text
1715
         *
1716
         * @name   Editor.setText
1717
         * @function
1718
         * @param {string} newText
1719
         */
1720
        me.setText = function(newText)
1721
        {
1722
            $message.val(newText);
1723
        }
1724
1725
        /**
1726
         * returns the current text
1727
         *
1728
         * @name   Editor.getText
1729
         * @function
1730
         * @return {string}
1731
         */
1732
        me.getText = function()
1733
        {
1734
            return $message.val()
1735
        }
1736
1737
        /**
1738
         * init status manager
1739
         *
1740
         * preloads jQuery elements
1741
         *
1742
         * @name   Editor.init
1743
         * @function
1744
         */
1745
        me.init = function()
1746
        {
1747
            $editorTabs = $('#editorTabs');
1748
            $message = $('#message');
1749
1750
            // bind events
1751
            $message.keydown(supportTabs);
1752
1753
            // bind click events to tab switchers (a), but save parent of them
1754
            // (li)
1755
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1756
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1757
        }
1758
1759
        return me;
1760
    })(window, document);
1761
1762
    /**
1763
     * (view) Parse and show paste.
1764
     *
1765
     * @param  {object} window
1766
     * @param  {object} document
1767
     * @class
1768
     */
1769
    var PasteViewer = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter document is not used and could be removed.

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

Loading history...
1770
        var me = {};
1771
1772
        var $placeholder,
1773
            $prettyMessage,
1774
            $prettyPrint,
1775
            $plainText;
1776
1777
        var text,
1778
            format = 'plaintext',
1779
            isDisplayed = false,
1780
            isChanged = true; // by default true as nothing was parsed yet
1781
1782
        /**
1783
         * apply the set format on paste and displays it
1784
         *
1785
         * @private
1786
         * @function
1787
         */
1788
        function parsePaste()
1789
        {
1790
            // skip parsing if no text is given
1791
            if (text === '') {
1792
                return;
1793
            }
1794
1795
            // set text
1796
            Helper.setElementText($plainText, text);
1797
            Helper.setElementText($prettyPrint, text);
1798
1799
            switch (format) {
1800
                case 'markdown':
1801
                    var converter = new showdown.Converter({
1802
                        strikethrough: true,
1803
                        tables: true,
1804
                        tablesHeaderId: true
1805
                    });
1806
                    $plainText.html(
1807
                        converter.makeHtml(text)
1808
                    );
1809
                    // add table classes from bootstrap css
1810
                    $plainText.find('table').addClass('table-condensed table-bordered');
1811
                    break;
1812
                case 'syntaxhighlighting':
1813
                    // @TODO is this really needed or is "one" enough?
1814
                    if (typeof prettyPrint === 'function')
1815
                    {
1816
                        prettyPrint();
1817
                    }
1818
1819
                    $prettyPrint.html(
1820
                        prettyPrintOne(
1821
                            Helper.htmlEntities(text), null, true
1822
                        )
1823
                    );
1824
                    // fall through, as the rest is the same
1825
                default: // = 'plaintext'
1826
                    // convert URLs to clickable links
1827
                    Helper.urls2links($plainText);
1828
                    Helper.urls2links($prettyPrint);
1829
1830
                    $prettyPrint.css('white-space', 'pre-wrap');
1831
                    $prettyPrint.css('word-break', 'normal');
1832
                    $prettyPrint.removeClass('prettyprint');
1833
            }
1834
        }
1835
1836
        /**
1837
         * displays the paste
1838
         *
1839
         * @private
1840
         * @function
1841
         */
1842
        function showPaste()
1843
        {
1844
            // instead of "nothing" better display a placeholder
1845
            if (text === '') {
1846
                $placeholder.removeClass('hidden')
1847
                return;
1848
            }
1849
            // otherwise hide the placeholder
1850
            $placeholder.addClass('hidden')
1851
1852
            switch (format) {
1853
                case 'markdown':
1854
                    $plainText.removeClass('hidden');
1855
                    $prettyMessage.addClass('hidden');
1856
                    break;
1857
                default:
1858
                    $plainText.addClass('hidden');
1859
                    $prettyMessage.removeClass('hidden');
1860
                    break;
1861
            }
1862
        }
1863
1864
        /**
1865
         * sets the format in which the text is shown
1866
         *
1867
         * @name   PasteViewer.setFormat
1868
         * @function
1869
         * @param {string} newFormat the the new format
1870
         */
1871
        me.setFormat = function(newFormat)
1872
        {
1873
            // skip if there is no update
1874
            if (format === newFormat) {
1875
                return;
1876
            }
1877
1878
            // needs to update display too, if from or to Markdown is switched
1879
            if (format === 'markdown' || newFormat === 'markdown') {
1880
                isDisplayed = false;
1881
            }
1882
1883
            format = newFormat;
1884
            isChanged = true;
1885
        }
1886
1887
        /**
1888
         * returns the current format
1889
         *
1890
         * @name   PasteViewer.setFormat
1891
         * @function
1892
         * @return {string}
1893
         */
1894
        me.getFormat = function()
1895
        {
1896
            return format;
1897
        }
1898
1899
        /**
1900
         * returns whether the current view is pretty printed
1901
         *
1902
         * @name   PasteViewer.isPrettyPrinted
1903
         * @function
1904
         * @return {bool}
1905
         */
1906
        me.isPrettyPrinted = function()
1907
        {
1908
            return $prettyPrint.hasClass('prettyprinted');
1909
        }
1910
1911
        /**
1912
         * sets the text to show
1913
         *
1914
         * @name   PasteViewer.setText
1915
         * @function
1916
         * @param {string} newText the text to show
1917
         */
1918
        me.setText = function(newText)
1919
        {
1920
            if (text !== newText) {
1921
                text = newText;
1922
                isChanged = true;
1923
            }
1924
        }
1925
1926
        /**
1927
         * gets the current cached text
1928
         *
1929
         * @name   PasteViewer.getText
1930
         * @function
1931
         * @return {string}
1932
         */
1933
        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...
1934
        {
1935
            return text;
1936
        }
1937
1938
        /**
1939
         * show/update the parsed text (preview)
1940
         *
1941
         * @name   PasteViewer.run
1942
         * @function
1943
         */
1944
        me.run = function()
1945
        {
1946
            if (isChanged) {
1947
                parsePaste();
1948
                isChanged = false;
1949
            }
1950
1951
            if (!isDisplayed) {
1952
                showPaste();
1953
                isDisplayed = true;
1954
            }
1955
        }
1956
1957
        /**
1958
         * hide parsed text (preview)
1959
         *
1960
         * @name   PasteViewer.hide
1961
         * @function
1962
         */
1963
        me.hide = function()
1964
        {
1965
            if (!isDisplayed) {
1966
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1967
            }
1968
1969
            $plainText.addClass('hidden');
1970
            $prettyMessage.addClass('hidden');
1971
            $placeholder.addClass('hidden');
1972
1973
            isDisplayed = false;
1974
        }
1975
1976
        /**
1977
         * init status manager
1978
         *
1979
         * preloads jQuery elements
1980
         *
1981
         * @name   Editor.init
1982
         * @function
1983
         */
1984
        me.init = function()
1985
        {
1986
            $placeholder = $('#placeholder');
1987
            $plainText = $('#plaintext');
1988
            $prettyMessage = $('#prettymessage');
1989
            $prettyPrint = $('#prettyprint');
1990
1991
            // check requirements
1992
            if (typeof prettyPrintOne !== 'function') {
1993
                Alert.showError([
1994
                    'The library %s is not available. This may cause display errors.',
1995
                    'pretty print'
1996
                ]);
1997
            }
1998
            if (typeof showdown !== 'object') {
1999
                Alert.showError([
2000
                    'The library %s is not available. This may cause display errors.',
2001
                    'showdown'
2002
                ]);
2003
            }
2004
2005
            // get default option from template/HTML or fall back to set value
2006
            format = Model.getFormatDefault() || format;
2007
        }
2008
2009
        return me;
2010
    })(window, document);
2011
2012
    /**
2013
     * (view) Show attachment and preview if possible
2014
     *
2015
     * @param  {object} window
2016
     * @param  {object} document
2017
     * @class
2018
     */
2019
    var AttachmentViewer = (function (window, document) {
2020
        var me = {};
2021
2022
        var $attachmentLink,
2023
            $attachmentPreview,
2024
            $attachment;
2025
2026
        var attachmentChanged = false,
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2027
            attachmentHasPreview = false;
2028
2029
        /**
2030
         * sets the attachment but does not yet show it
2031
         *
2032
         * @name   AttachmentViewer.setAttachment
2033
         * @function
2034
         * @param {string} attachmentData - base64-encoded data of file
2035
         * @param {string} fileName - optional, file name
2036
         */
2037
        me.setAttachment = function(attachmentData, fileName)
2038
        {
2039
            var imagePrefix = 'data:image/';
2040
2041
            $attachmentLink.attr('href', attachmentData);
2042
            if (typeof fileName !== 'undefined') {
2043
                $attachmentLink.attr('download', fileName);
2044
            }
2045
2046
            // if the attachment is an image, display it
2047
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2048
                $attachmentPreview.html(
2049
                    $(document.createElement('img'))
2050
                        .attr('src', attachmentData)
2051
                        .attr('class', 'img-thumbnail')
2052
                );
2053
                attachmentHasPreview = true;
2054
            }
2055
2056
            attachmentChanged = true;
0 ignored issues
show
Unused Code introduced by
The variable attachmentChanged seems to be never used. Consider removing it.
Loading history...
2057
        }
2058
2059
        /**
2060
         * displays the attachment
2061
         *
2062
         * @name AttachmentViewer.showAttachment
2063
         * @function
2064
         */
2065
        me.showAttachment = function()
2066
        {
2067
            $attachment.removeClass('hidden');
2068
2069
            if (attachmentHasPreview) {
2070
                $attachmentPreview.removeClass('hidden');
2071
            }
2072
        }
2073
2074
        /**
2075
         * removes the attachment
2076
         *
2077
         * This automatically hides the attachment containers to, to
2078
         * prevent an inconsistent display.
2079
         *
2080
         * @name AttachmentViewer.removeAttachment
2081
         * @function
2082
         */
2083
        me.removeAttachment = function()
2084
        {
2085
            me.hideAttachment();
2086
            me.hideAttachmentPreview();
2087
            $attachmentLink.prop('href', '');
2088
            $attachmentLink.prop('download', '');
2089
            $attachmentPreview.html('');
2090
        }
2091
2092
        /**
2093
         * hides the attachment
2094
         *
2095
         * This will not hide the preview {@see me.hideAttachmentPreview}
2096
         * nor will it hide the attachment link if it was moved somewhere
2097
         * else {@see moveAttachmentTo}.
2098
         *
2099
         * @name AttachmentViewer.hideAttachment
2100
         * @function
2101
         */
2102
        me.hideAttachment = function()
2103
        {
2104
            $attachment.addClass('hidden');
2105
        }
2106
2107
        /**
2108
         * hides the attachment preview
2109
         *
2110
         * @name AttachmentViewer.hideAttachmentPreview
2111
         * @function
2112
         */
2113
        me.hideAttachmentPreview = function()
2114
        {
2115
            $attachmentPreview.addClass('hidden');
2116
        }
2117
2118
        /**
2119
         * checks if there is an attachment
2120
         *
2121
         * @name   AttachmentViewer.hasAttachment
2122
         * @function
2123
         */
2124
        me.hasAttachment = function()
2125
        {
2126
            return ($attachmentLink.prop('href') !== '')
2127
        }
2128
2129
        /**
2130
         * return the attachment
2131
         *
2132
         * @name   AttachmentViewer.getAttachment
2133
         * @function
2134
         * @returns {array}
2135
         */
2136
        me.getAttachment = function()
2137
        {
2138
            return [
2139
                $attachmentLink.prop('href'),
2140
                $attachmentLink.prop('download')
2141
            ];
2142
        }
2143
2144
        /**
2145
         * moves the attachment link to another element
2146
         *
2147
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2148
         *
2149
         * @name   AttachmentViewer.setClonedAttachment
2150
         * @function
2151
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2152
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2153
         */
2154
        me.moveAttachmentTo = function($element, label)
2155
        {
2156
            // move elemement to new place
2157
            $attachmentLink.appendTo($element);
2158
2159
            // update text
2160
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2161
        }
2162
2163
        /**
2164
         * initiate
2165
         *
2166
         * preloads jQuery elements
2167
         *
2168
         * @name   AttachmentViewer.init
2169
         * @function
2170
         */
2171
        me.init = function()
2172
        {
2173
            $attachment = $('#attachment');
2174
            $attachmentLink = $('#attachment a');
2175
            $attachmentPreview = $('#attachmentPreview');
2176
        }
2177
2178
        return me;
2179
    })(window, document);
2180
2181
    /**
2182
     * (view) Shows discussion thread and handles replies
2183
     *
2184
     * @param  {object} window
2185
     * @param  {object} document
2186
     * @class
2187
     */
2188
    var DiscussionViewer = (function (window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter window is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter document is not used and could be removed.

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

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