Passed
Push — master ( da45d3...429d43 )
by rugk
03:45
created

AttachmentViewer.constructor   B

Complexity

Conditions 1
Paths 16

Size

Total Lines 463

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 1
c 5
b 1
f 0
nc 16
nop 0
dl 0
loc 463
rs 8.2857

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