Passed
Push — master ( 57754f...4ccb4b )
by El
03:38
created

me.refreshPaste   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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