Passed
Push — master ( 76c147...6eb882 )
by El
04:25 queued 01:03
created

privatebin.js ➔ displayQrCode   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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