Passed
Push — master ( 81ac23...a5d5f6 )
by El
03:02
created

js/privatebin.js (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

Code
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: FileReader */
16
/** global: RawDeflate */
17
/** global: history */
18
/** global: navigator */
19
/** global: prettyPrint */
20
/** global: prettyPrintOne */
21
/** global: showdown */
22
/** global: sjcl */
23
24
// Immediately start random number generator collector.
25
sjcl.random.startCollectors();
26
27
// main application start, called when DOM is fully loaded
28
jQuery(document).ready(function() {
29
    // run main controller
30
    $.PrivateBin.Controller.init();
31
});
32
33
jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
34
    'use strict';
35
36
    /**
37
     * static Helper methods
38
     *
39
     * @name Helper
40
     * @class
41
     */
42
    var Helper = (function () {
43
        var me = {};
44
45
        /**
46
         * character to HTML entity lookup table
47
         *
48
         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
49
         * @name Helper.entityMap
50
         * @private
51
         * @enum   {Object}
52
         * @readonly
53
         */
54
        var entityMap = {
55
            '&': '&',
56
            '<': '&lt;',
57
            '>': '&gt;',
58
            '"': '&quot;',
59
            "'": '&#39;',
60
            '/': '&#x2F;',
61
            '`': '&#x60;',
62
            '=': '&#x3D;'
63
        };
64
65
        /**
66
         * cache for script location
67
         *
68
         * @name Helper.baseUri
69
         * @private
70
         * @enum   {string|null}
71
         */
72
        var baseUri = null;
73
74
        /**
75
         * converts a duration (in seconds) into human friendly approximation
76
         *
77
         * @name Helper.secondsToHuman
78
         * @function
79
         * @param  {number} seconds
80
         * @return {Array}
81
         */
82
        me.secondsToHuman = function(seconds)
83
        {
84
            var v;
85
            if (seconds < 60)
86
            {
87
                v = Math.floor(seconds);
88
                return [v, 'second'];
89
            }
90
            if (seconds < 60 * 60)
91
            {
92
                v = Math.floor(seconds / 60);
93
                return [v, 'minute'];
94
            }
95
            if (seconds < 60 * 60 * 24)
96
            {
97
                v = Math.floor(seconds / (60 * 60));
98
                return [v, 'hour'];
99
            }
100
            // If less than 2 months, display in days:
101
            if (seconds < 60 * 60 * 24 * 60)
102
            {
103
                v = Math.floor(seconds / (60 * 60 * 24));
104
                return [v, 'day'];
105
            }
106
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
107
            return [v, 'month'];
108
        }
109
110
        /**
111
         * text range selection
112
         *
113
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
114
         * @name   Helper.selectText
115
         * @function
116
         * @param  {HTMLElement} element
117
         */
118
        me.selectText = function(element)
119
        {
120
            var range, selection;
121
122
            // MS
123
            if (document.body.createTextRange) {
124
                range = document.body.createTextRange();
125
                range.moveToElementText(element);
126
                range.select();
127
            } else if (window.getSelection){
128
                selection = window.getSelection();
129
                range = document.createRange();
130
                range.selectNodeContents(element);
131
                selection.removeAllRanges();
132
                selection.addRange(range);
133
            }
134
        }
135
136
        /**
137
         * set text of a jQuery element (required for IE),
138
         *
139
         * @name   Helper.setElementText
140
         * @function
141
         * @param  {jQuery} $element - a jQuery element
142
         * @param  {string} text - the text to enter
143
         */
144
        me.setElementText = function($element, text)
145
        {
146
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
147
            if ($('#oldienotice').is(':visible')) {
148
                var html = me.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
149
                $element.html('<pre>' + html + '</pre>');
150
            }
151
            // for other (sane) browsers:
152
            else
153
            {
154
                $element.text(text);
155
            }
156
        }
157
158
        /**
159
         * convert URLs to clickable links.
160
         * URLs to handle:
161
         * <pre>
162
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
163
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
164
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
165
         * </pre>
166
         *
167
         * @name   Helper.urls2links
168
         * @function
169
         * @param  {Object} $element - a jQuery DOM element
170
         */
171
        me.urls2links = function($element)
172
        {
173
            var markup = '<a href="$1" rel="nofollow">$1</a>';
174
            $element.html(
175
                $element.html().replace(
176
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+*-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
177
                    markup
178
                )
179
            );
180
            $element.html(
181
                $element.html().replace(
182
                    /((magnet):[\w?=&.\/-;#@~%+*-]+)/ig,
183
                    markup
184
                )
185
            );
186
        }
187
188
        /**
189
         * minimal sprintf emulation for %s and %d formats
190
         *
191
         * Note that this function needs the parameters in the same order as the
192
         * format strings appear in the string, contrary to the original.
193
         *
194
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
195
         * @name   Helper.sprintf
196
         * @function
197
         * @param  {string} format
0 ignored issues
show
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
198
         * @param  {...*} args - one or multiple parameters injected into format string
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
199
         * @return {string}
200
         */
201
        me.sprintf = function()
202
        {
203
            var args = Array.prototype.slice.call(arguments);
204
            var format = args[0],
205
                i = 1;
206
            return format.replace(/%(s|d)/g, function (m) {
207
                // m is the matched format, e.g. %s, %d
208
                var val = args[i];
209
                // A switch statement so that the formatter can be extended.
210
                switch (m)
211
                {
212
                    case '%d':
213
                        val = parseFloat(val);
214
                        if (isNaN(val)) {
215
                            val = 0;
216
                        }
217
                        break;
218
                    default:
219
                        // Default is %s
220
                }
221
                ++i;
222
                return val;
223
            });
224
        }
225
226
        /**
227
         * get value of cookie, if it was set, empty string otherwise
228
         *
229
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
230
         * @name   Helper.getCookie
231
         * @function
232
         * @param  {string} cname - may not be empty
233
         * @return {string}
234
         */
235
        me.getCookie = function(cname) {
236
            var name = cname + '=',
237
                ca = document.cookie.split(';');
238
            for (var i = 0; i < ca.length; ++i) {
239
                var c = ca[i];
240
                while (c.charAt(0) === ' ')
241
                {
242
                    c = c.substring(1);
243
                }
244
                if (c.indexOf(name) === 0)
245
                {
246
                    return c.substring(name.length, c.length);
247
                }
248
            }
249
            return '';
250
        }
251
252
        /**
253
         * get the current location (without search or hash part of the URL),
254
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
255
         *
256
         * @name   Helper.baseUri
257
         * @function
258
         * @return {string}
259
         */
260
        me.baseUri = function()
261
        {
262
            // check for cached version
263
            if (baseUri !== null) {
264
                return baseUri;
265
            }
266
267
            baseUri = window.location.origin + window.location.pathname;
268
            return baseUri;
269
        }
270
271
        /**
272
         * convert all applicable characters to HTML entities
273
         *
274
         * @see    {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
275
         * @name   Helper.htmlEntities
276
         * @function
277
         * @param  {string} str
278
         * @return {string} escaped HTML
279
         */
280
        me.htmlEntities = function(str) {
281
            return String(str).replace(
282
                /[&<>"'`=\/]/g, function(s) {
283
                    return entityMap[s];
284
                });
285
        }
286
287
        /**
288
         * resets state, used for unit testing
289
         *
290
         * @name   Helper.reset
291
         * @function
292
         */
293
        me.reset = function()
294
        {
295
            baseUri = null;
296
        }
297
298
        return me;
299
    })();
300
301
    /**
302
     * internationalization module
303
     *
304
     * @name I18n
305
     * @param  {object} window
306
     * @param  {object} document
307
     * @class
308
     */
309
    var I18n = (function () {
310
        var me = {};
311
312
        /**
313
         * const for string of loaded language
314
         *
315
         * @name I18n.languageLoadedEvent
316
         * @private
317
         * @prop   {string}
318
         * @readonly
319
         */
320
        var languageLoadedEvent = 'languageLoaded';
321
322
        /**
323
         * supported languages, minus the built in 'en'
324
         *
325
         * @name I18n.supportedLanguages
326
         * @private
327
         * @prop   {string[]}
328
         * @readonly
329
         */
330
        var supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'];
331
332
        /**
333
         * built in language
334
         *
335
         * @name I18n.language
336
         * @private
337
         * @prop   {string|null}
338
         */
339
        var language = null;
340
341
        /**
342
         * translation cache
343
         *
344
         * @name I18n.translations
345
         * @private
346
         * @enum   {Object}
347
         */
348
        var translations = {};
349
350
        /**
351
         * translate a string, alias for I18n.translate
352
         *
353
         * @name   I18n._
354
         * @function
355
         * @param  {jQuery} $element - optional
356
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
357
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
358
         * @return {string}
359
         */
360
        me._ = function()
361
        {
362
            return me.translate.apply(this, arguments);
363
        }
364
365
        /**
366
         * translate a string
367
         *
368
         * Optionally pass a jQuery element as the first parameter, to automatically
369
         * let the text of this element be replaced. In case the (asynchronously
370
         * loaded) language is not downloadet yet, this will make sure the string
371
         * is replaced when it is actually loaded.
372
         * So for easy translations passing the jQuery object to apply it to is
373
         * more save, especially when they are loaded in the beginning.
374
         *
375
         * @name   I18n.translate
376
         * @function
377
         * @param  {jQuery} $element - optional
378
         * @param  {string} messageId
0 ignored issues
show
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
379
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
380
         * @return {string}
381
         */
382
        me.translate = function()
383
        {
384
            // convert parameters to array
385
            var args = Array.prototype.slice.call(arguments),
386
                messageId,
387
                $element = null;
388
389
            // parse arguments
390
            if (args[0] instanceof jQuery) {
391
                // optional jQuery element as first parameter
392
                $element = args[0];
393
                args.shift();
394
            }
395
396
            // extract messageId from arguments
397
            var usesPlurals = $.isArray(args[0]);
398
            if (usesPlurals) {
399
                // use the first plural form as messageId, otherwise the singular
400
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
401
            } else {
402
                messageId = args[0];
403
            }
404
405
            if (messageId.length === 0) {
406
                return messageId;
407
            }
408
409
            // if no translation string cannot be found (in translations object)
410
            if (!translations.hasOwnProperty(messageId) || language === null) {
411
                // if language is still loading and we have an elemt assigned
412
                if (language === null && $element !== null) {
413
                    // handle the error by attaching the language loaded event
414
                    var orgArguments = arguments;
415
                    $(document).on(languageLoadedEvent, function () {
416
                        // log to show that the previous error could be mitigated
417
                        console.log('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language);
418
                        // re-execute this function
419
                        me.translate.apply(this, orgArguments);
420
                    });
421
422
                    // and fall back to English for now until the real language
423
                    // file is loaded
424
                }
425
426
                // for all other langauges than English for which this behaviour
427
                // is expected as it is built-in, log error
428
                if (language !== null && language !== 'en') {
429
                    console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
430
                    // fallback to English
431
                }
432
433
                // save English translation (should be the same on both sides)
434
                translations[messageId] = args[0];
435
            }
436
437
            // lookup plural translation
438
            if (usesPlurals && $.isArray(translations[messageId])) {
439
                var n = parseInt(args[1] || 1, 10),
440
                    key = me.getPluralForm(n),
441
                    maxKey = translations[messageId].length - 1;
442
                if (key > maxKey) {
443
                    key = maxKey;
444
                }
445
                args[0] = translations[messageId][key];
446
                args[1] = n;
447
            } else {
448
                // lookup singular translation
449
                args[0] = translations[messageId];
450
            }
451
452
            // format string
453
            var output = Helper.sprintf.apply(this, args);
454
455
            // if $element is given, apply text to element
456
            if ($element !== null) {
457
                // get last text node of element
458
                var content = $element.contents();
459
                if (content.length > 1) {
460
                    content[content.length - 1].nodeValue = ' ' + output;
461
                } else {
462
                    $element.text(output);
463
                }
464
            }
465
466
            return output;
467
        }
468
469
        /**
470
         * per language functions to use to determine the plural form
471
         *
472
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
473
         * @name   I18n.getPluralForm
474
         * @function
475
         * @param  {int} n
476
         * @return {int} array key
477
         */
478
        me.getPluralForm = function(n) {
479
            switch (language)
480
            {
481
                case 'fr':
482
                case 'oc':
483
                case 'zh':
484
                    return (n > 1 ? 1 : 0);
485
                case 'pl':
486
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
487
                case 'ru':
488
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
489
                case 'sl':
490
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
491
                // de, en, es, it, no, pt
492
                default:
493
                    return (n !== 1 ? 1 : 0);
494
            }
495
        }
496
497
        /**
498
         * load translations into cache
499
         *
500
         * @name   I18n.loadTranslations
501
         * @function
502
         */
503
        me.loadTranslations = function()
504
        {
505
            var newLanguage = Helper.getCookie('lang');
506
507
            // auto-select language based on browser settings
508
            if (newLanguage.length === 0) {
509
                newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
510
            }
511
512
            // if language is already used skip update
513
            if (newLanguage === language) {
514
                return;
515
            }
516
517
            // if language is built-in (English) skip update
518
            if (newLanguage === 'en') {
519
                language = 'en';
520
                return;
521
            }
522
523
            // if language is not supported, show error
524
            if (supportedLanguages.indexOf(newLanguage) === -1) {
525
                console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
526
                language = 'en';
527
                return;
528
            }
529
530
            // load strings from JSON
531
            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
532
                language = newLanguage;
533
                translations = data;
534
                $(document).triggerHandler(languageLoadedEvent);
535
            }).fail(function (data, textStatus, errorMsg) {
536
                console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
537
                language = 'en';
538
            });
539
        }
540
541
        /**
542
         * resets state, used for unit testing
543
         *
544
         * @name   I18n.reset
545
         * @function
546
         */
547
        me.reset = function(mockLanguage, mockTranslations)
548
        {
549
            language = mockLanguage || null;
550
            translations = mockTranslations || {};
551
        }
552
553
        return me;
554
    })();
555
556
    /**
557
     * handles everything related to en/decryption
558
     *
559
     * @name CryptTool
560
     * @class
561
     */
562
    var CryptTool = (function () {
563
        var me = {};
564
565
        /**
566
         * compress a message (deflate compression), returns base64 encoded data
567
         *
568
         * @name   CryptTool.compress
569
         * @function
570
         * @private
571
         * @param  {string} message
572
         * @return {string} base64 data
573
         */
574
        function compress(message)
575
        {
576
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
577
        }
578
579
        /**
580
         * decompress a message compressed with cryptToolcompress()
581
         *
582
         * @name   CryptTool.decompress
583
         * @function
584
         * @private
585
         * @param  {string} data - base64 data
586
         * @return {string} message
587
         */
588
        function decompress(data)
589
        {
590
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
591
        }
592
593
        /**
594
         * compress, then encrypt message with given key and password
595
         *
596
         * @name   CryptTool.cipher
597
         * @function
598
         * @param  {string} key
599
         * @param  {string} password
600
         * @param  {string} message
601
         * @return {string} data - JSON with encrypted data
602
         */
603
        me.cipher = function(key, password, message)
604
        {
605
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
606
            var options = {
607
                mode: 'gcm',
608
                ks: 256,
609
                ts: 128
610
            };
611
612
            if ((password || '').trim().length === 0) {
613
                return sjcl.encrypt(key, compress(message), options);
614
            }
615
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
616
        }
617
618
        /**
619
         * decrypt message with key, then decompress
620
         *
621
         * @name   CryptTool.decipher
622
         * @function
623
         * @param  {string} key
624
         * @param  {string} password
625
         * @param  {string} data - JSON with encrypted data
626
         * @return {string} decrypted message
627
         */
628
        me.decipher = function(key, password, data)
629
        {
630
            if (data !== undefined) {
631
                try {
632
                    return decompress(sjcl.decrypt(key, data));
633
                } catch(err) {
634
                    try {
635
                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
636
                    } catch(e) {
637
                        // ignore error, because ????? @TODO
638
                    }
639
                }
640
            }
641
            return '';
642
        }
643
644
        /**
645
         * checks whether the crypt tool has collected enough entropy
646
         *
647
         * @name   CryptTool.isEntropyReady
648
         * @function
649
         * @return {bool}
650
         */
651
        me.isEntropyReady = function()
652
        {
653
            return sjcl.random.isReady();
654
        }
655
656
        /**
657
         * add a listener function, triggered when enough entropy is available
658
         *
659
         * @name   CryptTool.addEntropySeedListener
660
         * @function
661
         * @param {function} func
662
         */
663
        me.addEntropySeedListener = function(func)
664
        {
665
            sjcl.random.addEventListener('seeded', func);
666
        }
667
668
        /**
669
         * returns a random symmetric key
670
         *
671
         * @name   CryptTool.getSymmetricKey
672
         * @function
673
         * @return {string} func
674
         */
675
        me.getSymmetricKey = function()
676
        {
677
            return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
678
        }
679
680
        return me;
681
    })();
682
683
    /**
684
     * (Model) Data source (aka MVC)
685
     *
686
     * @name   Model
687
     * @class
688
     */
689
    var Model = (function () {
690
        var me = {};
691
692
        var $cipherData,
693
            $templates;
694
695
        var id = null, symmetricKey = null;
696
697
        /**
698
         * returns the expiration set in the HTML
699
         *
700
         * @name   Model.getExpirationDefault
701
         * @function
702
         * @return string
703
         * @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
704
         */
705
        me.getExpirationDefault = function()
706
        {
707
            return $('#pasteExpiration').val();
708
        }
709
710
        /**
711
         * returns the format set in the HTML
712
         *
713
         * @name   Model.getFormatDefault
714
         * @function
715
         * @return string
716
         * @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
717
         */
718
        me.getFormatDefault = function()
719
        {
720
            return $('#pasteFormatter').val();
721
        }
722
723
        /**
724
         * check if cipher data was supplied
725
         *
726
         * @name   Model.getCipherData
727
         * @function
728
         * @return boolean
729
         */
730
        me.hasCipherData = function()
731
        {
732
            return (me.getCipherData().length > 0);
733
        }
734
735
        /**
736
         * returns the cipher data
737
         *
738
         * @name   Model.getCipherData
739
         * @function
740
         * @return string
741
         */
742
        me.getCipherData = function()
743
        {
744
            return $cipherData.text();
745
        }
746
747
        /**
748
         * get the pastes unique identifier from the URL,
749
         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
750
         *
751
         * @name   Model.getPasteId
752
         * @function
753
         * @return {string} unique identifier
754
         * @throws {string}
755
         */
756
        me.getPasteId = function()
757
        {
758
            if (id === null) {
759
                id = window.location.search.substring(1);
760
761
                if (id === '') {
762
                    throw 'no paste id given';
763
                }
764
            }
765
766
            return id;
767
        }
768
769
        /**
770
         * return the deciphering key stored in anchor part of the URL
771
         *
772
         * @name   Model.getPasteKey
773
         * @function
774
         * @return {string|null} key
775
         * @throws {string}
776
         */
777
        me.getPasteKey = function()
778
        {
779
            if (symmetricKey === null) {
780
                symmetricKey = window.location.hash.substring(1);
781
782
                if (symmetricKey === '') {
783
                    throw 'no encryption key given';
784
                }
785
786
                // Some web 2.0 services and redirectors add data AFTER the anchor
787
                // (such as &utm_source=...). We will strip any additional data.
788
                var ampersandPos = symmetricKey.indexOf('&');
789
                if (ampersandPos > -1)
790
                {
791
                    symmetricKey = symmetricKey.substring(0, ampersandPos);
792
                }
793
            }
794
795
            return symmetricKey;
796
        }
797
798
        /**
799
         * returns a jQuery copy of the HTML template
800
         *
801
         * @name Model.getTemplate
802
         * @function
803
         * @param  {string} name - the name of the template
804
         * @return {jQuery}
805
         */
806
        me.getTemplate = function(name)
807
        {
808
            // find template
809
            var $element = $templates.find('#' + name + 'template').clone(true);
810
            // change ID to avoid collisions (one ID should really be unique)
811
            return $element.prop('id', name);
812
        }
813
814
        /**
815
         * resets state, used for unit testing
816
         *
817
         * @name   Model.reset
818
         * @function
819
         */
820
        me.reset = function()
821
        {
822
            $cipherData = $templates = id = symmetricKey = null;
823
        }
824
825
        /**
826
         * init navigation manager
827
         *
828
         * preloads jQuery elements
829
         *
830
         * @name   Model.init
831
         * @function
832
         */
833
        me.init = function()
834
        {
835
            $cipherData = $('#cipherdata');
836
            $templates = $('#templates');
837
        }
838
839
        return me;
840
    })();
841
842
    /**
843
     * Helper functions for user interface
844
     *
845
     * everything directly UI-related, which fits nowhere else
846
     *
847
     * @name   UiHelper
848
     * @param  {object} window
849
     * @param  {object} document
850
     * @class
851
     */
852
    var UiHelper = (function (window, document) {
853
        var me = {};
854
855
        /**
856
         * handle history (pop) state changes
857
         *
858
         * currently this does only handle redirects to the home page.
859
         *
860
         * @name   UiHelper.historyChange
861
         * @private
862
         * @function
863
         * @param  {Event} event
864
         */
865
        function historyChange(event)
866
        {
867
            var currentLocation = Helper.baseUri();
868
            if (event.originalEvent.state === null && // no state object passed
869
                event.originalEvent.target.location.href === currentLocation && // target location is home page
870
                window.location.href === currentLocation // and we are not already on the home page
871
            ) {
872
                // redirect to home page
873
                window.location.href = currentLocation;
874
            }
875
        }
876
877
        /**
878
         * reload the page
879
         *
880
         * This takes the user to the PrivateBin homepage.
881
         *
882
         * @name   UiHelper.reloadHome
883
         * @function
884
         */
885
        me.reloadHome = function()
886
        {
887
            window.location.href = Helper.baseUri();
888
        }
889
890
        /**
891
         * checks whether the element is currently visible in the viewport (so
892
         * the user can actually see it)
893
         *
894
         * @see    {@link https://stackoverflow.com/a/40658647}
895
         * @name   UiHelper.isVisible
896
         * @function
897
         * @param  {jQuery} $element The link hash to move to.
898
         */
899
        me.isVisible = function($element)
900
        {
901
            var elementTop = $element.offset().top;
902
            var viewportTop = $(window).scrollTop();
903
            var viewportBottom = viewportTop + $(window).height();
904
905
            return (elementTop > viewportTop && elementTop < viewportBottom);
906
        }
907
908
        /**
909
         * scrolls to a specific element
910
         *
911
         * @see    {@link https://stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
912
         * @name   UiHelper.scrollTo
913
         * @function
914
         * @param  {jQuery}           $element        The link hash to move to.
915
         * @param  {(number|string)}  animationDuration passed to jQuery .animate, when set to 0 the animation is skipped
916
         * @param  {string}           animationEffect   passed to jQuery .animate
917
         * @param  {function}         finishedCallback  function to call after animation finished
918
         */
919
        me.scrollTo = function($element, animationDuration, animationEffect, finishedCallback)
920
        {
921
            var $body = $('html, body'),
922
                margin = 50,
923
                callbackCalled = false;
924
925
            //calculate destination place
926
            var dest = 0;
927
            // if it would scroll out of the screen at the bottom only scroll it as
928
            // far as the screen can go
929
            if ($element.offset().top > $(document).height() - $(window).height()) {
930
                dest = $(document).height() - $(window).height();
931
            } else {
932
                dest = $element.offset().top - margin;
933
            }
934
            // skip animation if duration is set to 0
935
            if (animationDuration === 0) {
936
                window.scrollTo(0, dest);
937
            } else {
938
                // stop previous animation
939
                $body.stop();
940
                // scroll to destination
941
                $body.animate({
942
                    scrollTop: dest
943
                }, animationDuration, animationEffect);
944
            }
945
946
            // as we have finished we can enable scrolling again
947
            $body.queue(function (next) {
948
                if (!callbackCalled) {
949
                    // call user function if needed
950
                    if (typeof finishedCallback !== 'undefined') {
951
                        finishedCallback();
952
                    }
953
954
                    // prevent calling this function twice
955
                    callbackCalled = true;
956
                }
957
                next();
958
            });
959
        }
960
961
        /**
962
         * initialize
963
         *
964
         * @name   UiHelper.init
965
         * @function
966
         */
967
        me.init = function()
968
        {
969
            // update link to home page
970
            $('.reloadlink').prop('href', Helper.baseUri());
971
972
            $(window).on('popstate', historyChange);
973
        }
974
975
        return me;
976
    })(window, document);
977
978
    /**
979
     * Alert/error manager
980
     *
981
     * @name   Alert
982
     * @class
983
     */
984
    var Alert = (function () {
985
        var me = {};
986
987
        var $errorMessage,
988
            $loadingIndicator,
989
            $statusMessage,
990
            $remainingTime;
991
992
        var currentIcon = [
993
            'glyphicon-time', // loading icon
994
            'glyphicon-info-sign', // status icon
995
            '', // resevered for warning, not used yet
996
            'glyphicon-alert' // error icon
997
        ];
998
999
        var alertType = [
1000
            'loading', // not in bootstrap, but using a good value here
1001
            'info', // status icon
1002
            'warning', // not used yet
1003
            'danger' // error icon
1004
        ];
1005
1006
        var customHandler;
1007
1008
        /**
1009
         * forwards a request to the i18n module and shows the element
1010
         *
1011
         * @name   Alert.handleNotification
1012
         * @private
1013
         * @function
1014
         * @param  {int} id - id of notification
1015
         * @param  {jQuery} $element - jQuery object
1016
         * @param  {string|array} args
1017
         * @param  {string|null} icon - optional, icon
1018
         */
1019
        function handleNotification(id, $element, args, icon)
1020
        {
1021
            // basic parsing/conversion of parameters
1022
            if (typeof icon === 'undefined') {
1023
                icon = null;
1024
            }
1025
            if (typeof args === 'undefined') {
1026
                args = null;
1027
            } else if (typeof args === 'string') {
1028
                // convert string to array if needed
1029
                args = [args];
1030
            }
1031
1032
            // pass to custom handler if defined
1033
            if (typeof customHandler === 'function') {
1034
                var handlerResult = customHandler(alertType[id], $element, args, icon);
1035
                if (handlerResult === true) {
1036
                    // if it returs true, skip own handler
1037
                    return;
1038
                }
1039
                if (handlerResult instanceof jQuery) {
1040
                    // continue processing with new element
1041
                    $element = handlerResult;
1042
                    icon = null; // icons not supported in this case
1043
                }
1044
            }
1045
1046
            // handle icon
1047
            if (icon !== null && // icon was passed
1048
                icon !== currentIcon[id] // and it differs from current icon
1049
            ) {
1050
                var $glyphIcon = $element.find(':first');
1051
1052
                // remove (previous) icon
1053
                $glyphIcon.removeClass(currentIcon[id]);
1054
1055
                // any other thing as a string (e.g. 'null') (only) removes the icon
1056
                if (typeof icon === 'string') {
1057
                    // set new icon
1058
                    currentIcon[id] = 'glyphicon-' + icon;
1059
                    $glyphIcon.addClass(currentIcon[id]);
1060
                }
1061
            }
1062
1063
            // show text
1064
            if (args !== null) {
1065
                // add jQuery object to it as first parameter
1066
                args.unshift($element);
1067
                // pass it to I18n
1068
                I18n._.apply(this, args);
1069
            }
1070
1071
            // show notification
1072
            $element.removeClass('hidden');
1073
        }
1074
1075
        /**
1076
         * display a status message
1077
         *
1078
         * This automatically passes the text to I18n for translation.
1079
         *
1080
         * @name   Alert.showStatus
1081
         * @function
1082
         * @param  {string|array} message     string, use an array for %s/%d options
1083
         * @param  {string|null}  icon        optional, the icon to show,
1084
         *                                    default: leave previous icon
1085
         * @param  {bool}         dismissable optional, whether the notification
1086
         *                                    can be dismissed (closed), default: false
1087
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1088
         *                                    notification should be hidden automatically;
1089
         *                                    default: disabled (0); use true for default value
1090
         */
1091
        me.showStatus = function(message, icon, dismissable, autoclose)
1092
        {
1093
            console.log('status shown: ', message);
1094
            // @TODO: implement dismissable
1095
            // @TODO: implement autoclose
1096
1097
            handleNotification(1, $statusMessage, message, icon);
1098
        }
1099
1100
        /**
1101
         * display an error message
1102
         *
1103
         * This automatically passes the text to I18n for translation.
1104
         *
1105
         * @name   Alert.showError
1106
         * @function
1107
         * @param  {string|array} message     string, use an array for %s/%d options
1108
         * @param  {string|null}  icon        optional, the icon to show, default:
1109
         *                                    leave previous icon
1110
         * @param  {bool}         dismissable optional, whether the notification
1111
         *                                    can be dismissed (closed), default: false
1112
         * @param  {bool|int}     autoclose   optional, after how many seconds the
1113
         *                                    notification should be hidden automatically;
1114
         *                                    default: disabled (0); use true for default value
1115
         */
1116
        me.showError = function(message, icon, dismissable, autoclose)
1117
        {
1118
            console.error('error message shown: ', message);
1119
            // @TODO: implement dismissable (bootstrap add-on has it)
1120
            // @TODO: implement autoclose
1121
1122
            handleNotification(3, $errorMessage, message, icon);
1123
        }
1124
1125
        /**
1126
         * display remaining message
1127
         *
1128
         * This automatically passes the text to I18n for translation.
1129
         *
1130
         * @name   Alert.showRemaining
1131
         * @function
1132
         * @param  {string|array} message     string, use an array for %s/%d options
1133
         */
1134
        me.showRemaining = function(message)
1135
        {
1136
            console.log('remaining message shown: ', message);
0 ignored issues
show
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
1137
            handleNotification(1, $remainingTime, message);
1138
        }
1139
1140
        /**
1141
         * shows a loading message, optionally with a percentage
1142
         *
1143
         * This automatically passes all texts to the i10s module.
1144
         *
1145
         * @name   Alert.showLoading
1146
         * @function
1147
         * @param  {string|array|null} message      optional, use an array for %s/%d options, default: 'Loading…'
1148
         * @param  {int}               percentage   optional, default: null
1149
         * @param  {string|null}       icon         optional, the icon to show, default: leave previous icon
1150
         */
1151
        me.showLoading = function(message, percentage, icon)
1152
        {
1153
            if (typeof message !== 'undefined' && message !== null) {
1154
                console.log('status changed: ', message);
1155
            }
1156
1157
            // default message text
1158
            if (typeof message === 'undefined') {
1159
                message = 'Loading…';
1160
            }
1161
1162
            // currently percentage parameter is ignored
1163
            // // @TODO handle it here…
1164
1165
            handleNotification(0, $loadingIndicator, message, icon);
1166
1167
            // show loading status (cursor)
1168
            $('body').addClass('loading');
1169
        }
1170
1171
        /**
1172
         * hides the loading message
1173
         *
1174
         * @name   Alert.hideLoading
1175
         * @function
1176
         */
1177
        me.hideLoading = function()
1178
        {
1179
            $loadingIndicator.addClass('hidden');
1180
1181
            // hide loading cursor
1182
            $('body').removeClass('loading');
1183
        }
1184
1185
        /**
1186
         * hides any status/error messages
1187
         *
1188
         * This does not include the loading message.
1189
         *
1190
         * @name   Alert.hideMessages
1191
         * @function
1192
         */
1193
        me.hideMessages = function()
1194
        {
1195
            // also possible: $('.statusmessage').addClass('hidden');
1196
            $statusMessage.addClass('hidden');
1197
            $errorMessage.addClass('hidden');
1198
        }
1199
1200
        /**
1201
         * set a custom handler, which gets all notifications.
1202
         *
1203
         * This handler gets the following arguments:
1204
         * alertType (see array), $element, args, icon
1205
         * If it returns true, the own processing will be stopped so the message
1206
         * will not be displayed. Otherwise it will continue.
1207
         * As an aditional feature it can return q jQuery element, which will
1208
         * then be used to add the message there. Icons are not supported in
1209
         * that case and will be ignored.
1210
         * Pass 'null' to reset/delete the custom handler.
1211
         * Note that there is no notification when a message is supposed to get
1212
         * hidden.
1213
         *
1214
         * @name   Alert.setCustomHandler
1215
         * @function
1216
         * @param {function|null} newHandler
1217
         */
1218
        me.setCustomHandler = function(newHandler)
1219
        {
1220
            customHandler = newHandler;
1221
        }
1222
1223
        /**
1224
         * init status manager
1225
         *
1226
         * preloads jQuery elements
1227
         *
1228
         * @name   Alert.init
1229
         * @function
1230
         */
1231
        me.init = function()
1232
        {
1233
            // hide "no javascript" error message
1234
            $('#noscript').hide();
1235
1236
            // not a reset, but first set of the elements
1237
            $errorMessage = $('#errormessage');
1238
            $loadingIndicator = $('#loadingindicator');
1239
            $statusMessage = $('#status');
1240
            $remainingTime = $('#remainingtime');
1241
        }
1242
1243
        return me;
1244
    })();
1245
1246
    /**
1247
     * handles paste status/result
1248
     *
1249
     * @name   PasteStatus
1250
     * @param  {object} window
1251
     * @class
1252
     */
1253
    var PasteStatus = (function (window) {
1254
        var me = {};
1255
1256
        var $pasteSuccess,
1257
            $pasteUrl,
1258
            $remainingTime,
1259
            $shortenButton;
1260
1261
        /**
1262
         * forward to URL shortener
1263
         *
1264
         * @name   PasteStatus.sendToShortener
1265
         * @private
1266
         * @function
1267
         * @param  {Event} event
1268
         */
1269
        function sendToShortener(event)
1270
        {
1271
            window.location.href = $shortenButton.data('shortener')
1272
                                   + encodeURIComponent($pasteUrl.attr('href'));
1273
        }
1274
1275
        /**
1276
         * Forces opening the paste if the link does not do this automatically.
1277
         *
1278
         * This is necessary as browsers will not reload the page when it is
1279
         * already loaded (which is fake as it is set via history.pushState()).
1280
         *
1281
         * @name   PasteStatus.pasteLinkClick
1282
         * @function
1283
         * @param  {Event} event
1284
         */
1285
        function pasteLinkClick(event)
1286
        {
1287
            // check if location is (already) shown in URL bar
1288
            if (window.location.href === $pasteUrl.attr('href')) {
1289
                // if so we need to load link by reloading the current site
1290
                window.location.reload(true);
1291
            }
1292
        }
1293
1294
        /**
1295
         * creates a notification after a successfull paste upload
1296
         *
1297
         * @name   PasteStatus.createPasteNotification
1298
         * @function
1299
         * @param  {string} url
1300
         * @param  {string} deleteUrl
1301
         */
1302
        me.createPasteNotification = function(url, deleteUrl)
1303
        {
1304
            $('#pastelink').html(
1305
                I18n._(
1306
                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1307
                    url, url
1308
                )
1309
            );
1310
            // save newly created element
1311
            $pasteUrl = $('#pasteurl');
1312
            // and add click event
1313
            $pasteUrl.click(pasteLinkClick);
1314
1315
            // shorten button
1316
            $('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>');
1317
1318
            // show result
1319
            $pasteSuccess.removeClass('hidden');
1320
            // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1321
            Helper.selectText($pasteUrl[0]);
1322
        }
1323
1324
        /**
1325
         * shows the remaining time
1326
         *
1327
         * @name PasteStatus.showRemainingTime
1328
         * @function
1329
         * @param {object} pasteMetaData
1330
         */
1331
        me.showRemainingTime = function(pasteMetaData)
1332
        {
1333
            if (pasteMetaData.burnafterreading) {
1334
                // display paste "for your eyes only" if it is deleted
1335
1336
                // actually remove paste, before we claim it is deleted
1337
                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
1338
1339
                Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
1340
                $remainingTime.addClass('foryoureyesonly');
1341
1342
                // discourage cloning (it cannot really be prevented)
1343
                TopNav.hideCloneButton();
1344
1345
            } else if (pasteMetaData.expire_date) {
1346
                // display paste expiration
1347
                var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
1348
                    expirationLabel = [
1349
                        'This document will expire in %d ' + expiration[1] + '.',
1350
                        'This document will expire in %d ' + expiration[1] + 's.'
1351
                    ];
1352
1353
                Alert.showRemaining([expirationLabel, expiration[0]]);
1354
                $remainingTime.removeClass('foryoureyesonly')
1355
            } else {
1356
                // never expires
1357
                return;
1358
            }
1359
1360
            // in the end, display notification
1361
            $remainingTime.removeClass('hidden');
1362
        }
1363
1364
        /**
1365
         * hides the remaining time and successful upload notification
1366
         *
1367
         * @name PasteStatus.hideRemainingTime
1368
         * @function
1369
         */
1370
        me.hideMessages = function()
1371
        {
1372
            $remainingTime.addClass('hidden');
1373
            $pasteSuccess.addClass('hidden');
1374
        }
1375
1376
        /**
1377
         * init status manager
1378
         *
1379
         * preloads jQuery elements
1380
         *
1381
         * @name   PasteStatus.init
1382
         * @function
1383
         */
1384
        me.init = function()
1385
        {
1386
            $pasteSuccess = $('#pasteSuccess');
1387
            // $pasteUrl is saved in me.createPasteNotification() after creation
1388
            $remainingTime = $('#remainingtime');
1389
            $shortenButton = $('#shortenbutton');
1390
1391
            // bind elements
1392
            $shortenButton.click(sendToShortener);
1393
        }
1394
1395
        return me;
1396
    })(window);
1397
1398
    /**
1399
     * password prompt
1400
     *
1401
     * @name Prompt
1402
     * @class
1403
     */
1404
    var Prompt = (function () {
1405
        var me = {};
1406
1407
        var $passwordDecrypt,
1408
            $passwordForm,
1409
            $passwordModal;
1410
1411
        var password = '';
1412
1413
        /**
1414
         * submit a password in the modal dialog
1415
         *
1416
         * @name Prompt.submitPasswordModal
1417
         * @private
1418
         * @function
1419
         * @param  {Event} event
1420
         */
1421
        function submitPasswordModal(event)
1422
        {
1423
            event.preventDefault();
1424
1425
            // get input
1426
            password = $passwordDecrypt.val();
1427
1428
            // hide modal
1429
            $passwordModal.modal('hide');
1430
1431
            PasteDecrypter.run();
1432
        }
1433
1434
        /**
1435
         * ask the user for the password and set it
1436
         *
1437
         * @name Prompt.requestPassword
1438
         * @function
1439
         */
1440
        me.requestPassword = function()
1441
        {
1442
            // show new bootstrap method (if available)
1443
            if ($passwordModal.length !== 0) {
1444
                $passwordModal.modal({
1445
                    backdrop: 'static',
1446
                    keyboard: false
1447
                });
1448
                return;
1449
            }
1450
1451
            // fallback to old method for page template
1452
            var newPassword = prompt(I18n._('Please enter the password for this paste:'), '');
1453
            if (newPassword === null) {
1454
                throw 'password prompt canceled';
1455
            }
1456
            if (password.length === 0) {
1457
                // recursive…
1458
                return me.requestPassword();
1459
            }
1460
1461
            password = newPassword;
1462
        }
1463
1464
        /**
1465
         * getthe cached password
1466
         *
1467
         * If you do not get a password with this function
1468
         * (returns an empty string), use requestPassword.
1469
         *
1470
         * @name   Prompt.getPassword
1471
         * @function
1472
         * @return {string}
1473
         */
1474
        me.getPassword = function()
1475
        {
1476
            return password;
1477
        }
1478
1479
        /**
1480
         * init status manager
1481
         *
1482
         * preloads jQuery elements
1483
         *
1484
         * @name   Prompt.init
1485
         * @function
1486
         */
1487
        me.init = function()
1488
        {
1489
            $passwordDecrypt = $('#passworddecrypt');
1490
            $passwordForm = $('#passwordform');
1491
            $passwordModal = $('#passwordmodal');
1492
1493
            // bind events
1494
1495
            // focus password input when it is shown
1496
            $passwordModal.on('shown.bs.Model', function () {
1497
                $passwordDecrypt.focus();
1498
            });
1499
            // handle Model password submission
1500
            $passwordForm.submit(submitPasswordModal);
1501
        }
1502
1503
        return me;
1504
    })();
1505
1506
    /**
1507
     * Manage paste/message input, and preview tab
1508
     *
1509
     * Note that the actual preview is handled by PasteViewer.
1510
     *
1511
     * @name   Editor
1512
     * @class
1513
     */
1514
    var Editor = (function () {
1515
        var me = {};
1516
1517
        var $editorTabs,
1518
            $messageEdit,
1519
            $messagePreview,
1520
            $message;
1521
1522
        var isPreview = false;
1523
1524
        /**
1525
         * support input of tab character
1526
         *
1527
         * @name   Editor.supportTabs
1528
         * @function
1529
         * @param  {Event} event
1530
         * @this $message (but not used, so it is jQuery-free, possibly faster)
1531
         */
1532
        function supportTabs(event)
1533
        {
1534
            var keyCode = event.keyCode || event.which;
1535
            // tab was pressed
1536
            if (keyCode === 9) {
1537
                // get caret position & selection
1538
                var val   = this.value,
1539
                    start = this.selectionStart,
1540
                    end   = this.selectionEnd;
1541
                // set textarea value to: text before caret + tab + text after caret
1542
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1543
                // put caret at right position again
1544
                this.selectionStart = this.selectionEnd = start + 1;
1545
                // prevent the textarea to lose focus
1546
                event.preventDefault();
1547
            }
1548
        }
1549
1550
        /**
1551
         * view the Editor tab
1552
         *
1553
         * @name   Editor.viewEditor
1554
         * @function
1555
         * @param  {Event} event - optional
1556
         */
1557
        function viewEditor(event)
1558
        {
1559
            // toggle buttons
1560
            $messageEdit.addClass('active');
1561
            $messagePreview.removeClass('active');
1562
1563
            PasteViewer.hide();
1564
1565
            // reshow input
1566
            $message.removeClass('hidden');
1567
1568
            me.focusInput();
1569
1570
            // finish
1571
            isPreview = false;
1572
1573
            // prevent jumping of page to top
1574
            if (typeof event !== 'undefined') {
1575
                event.preventDefault();
1576
            }
1577
        }
1578
1579
        /**
1580
         * view the preview tab
1581
         *
1582
         * @name   Editor.viewPreview
1583
         * @function
1584
         * @param  {Event} event
1585
         */
1586
        function viewPreview(event)
1587
        {
1588
            // toggle buttons
1589
            $messageEdit.removeClass('active');
1590
            $messagePreview.addClass('active');
1591
1592
            // hide input as now preview is shown
1593
            $message.addClass('hidden');
1594
1595
            // show preview
1596
            PasteViewer.setText($message.val());
1597
            PasteViewer.run();
1598
1599
            // finish
1600
            isPreview = true;
1601
1602
            // prevent jumping of page to top
1603
            if (typeof event !== 'undefined') {
1604
                event.preventDefault();
1605
            }
1606
        }
1607
1608
        /**
1609
         * get the state of the preview
1610
         *
1611
         * @name   Editor.isPreview
1612
         * @function
1613
         */
1614
        me.isPreview = function()
1615
        {
1616
            return isPreview;
1617
        }
1618
1619
        /**
1620
         * reset the Editor view
1621
         *
1622
         * @name   Editor.resetInput
1623
         * @function
1624
         */
1625
        me.resetInput = function()
1626
        {
1627
            // go back to input
1628
            if (isPreview) {
1629
                viewEditor();
1630
            }
1631
1632
            // clear content
1633
            $message.val('');
1634
        }
1635
1636
        /**
1637
         * shows the Editor
1638
         *
1639
         * @name   Editor.show
1640
         * @function
1641
         */
1642
        me.show = function()
1643
        {
1644
            $message.removeClass('hidden');
1645
            $editorTabs.removeClass('hidden');
1646
        }
1647
1648
        /**
1649
         * hides the Editor
1650
         *
1651
         * @name   Editor.reset
1652
         * @function
1653
         */
1654
        me.hide = function()
1655
        {
1656
            $message.addClass('hidden');
1657
            $editorTabs.addClass('hidden');
1658
        }
1659
1660
        /**
1661
         * focuses the message input
1662
         *
1663
         * @name   Editor.focusInput
1664
         * @function
1665
         */
1666
        me.focusInput = function()
1667
        {
1668
            $message.focus();
1669
        }
1670
1671
        /**
1672
         * sets a new text
1673
         *
1674
         * @name   Editor.setText
1675
         * @function
1676
         * @param {string} newText
1677
         */
1678
        me.setText = function(newText)
1679
        {
1680
            $message.val(newText);
1681
        }
1682
1683
        /**
1684
         * returns the current text
1685
         *
1686
         * @name   Editor.getText
1687
         * @function
1688
         * @return {string}
1689
         */
1690
        me.getText = function()
1691
        {
1692
            return $message.val()
1693
        }
1694
1695
        /**
1696
         * init status manager
1697
         *
1698
         * preloads jQuery elements
1699
         *
1700
         * @name   Editor.init
1701
         * @function
1702
         */
1703
        me.init = function()
1704
        {
1705
            $editorTabs = $('#editorTabs');
1706
            $message = $('#message');
1707
1708
            // bind events
1709
            $message.keydown(supportTabs);
1710
1711
            // bind click events to tab switchers (a), but save parent of them
1712
            // (li)
1713
            $messageEdit = $('#messageedit').click(viewEditor).parent();
1714
            $messagePreview = $('#messagepreview').click(viewPreview).parent();
1715
        }
1716
1717
        return me;
1718
    })();
1719
1720
    /**
1721
     * (view) Parse and show paste.
1722
     *
1723
     * @name   PasteViewer
1724
     * @class
1725
     */
1726
    var PasteViewer = (function () {
1727
        var me = {};
1728
1729
        var $placeholder,
1730
            $prettyMessage,
1731
            $prettyPrint,
1732
            $plainText;
1733
1734
        var text,
1735
            format = 'plaintext',
1736
            isDisplayed = false,
1737
            isChanged = true; // by default true as nothing was parsed yet
1738
1739
        /**
1740
         * apply the set format on paste and displays it
1741
         *
1742
         * @name   PasteViewer.parsePaste
1743
         * @private
1744
         * @function
1745
         */
1746
        function parsePaste()
1747
        {
1748
            // skip parsing if no text is given
1749
            if (text === '') {
1750
                return;
1751
            }
1752
1753
            // set text
1754
            Helper.setElementText($plainText, text);
1755
            Helper.setElementText($prettyPrint, text);
1756
1757
            switch (format) {
1758
                case 'markdown':
1759
                    var converter = new showdown.Converter({
1760
                        strikethrough: true,
1761
                        tables: true,
1762
                        tablesHeaderId: true
1763
                    });
1764
                    $plainText.html(
1765
                        converter.makeHtml(text)
1766
                    );
1767
                    // add table classes from bootstrap css
1768
                    $plainText.find('table').addClass('table-condensed table-bordered');
1769
                    break;
1770
                case 'syntaxhighlighting':
1771
                    // @TODO is this really needed or is "one" enough?
1772
                    if (typeof prettyPrint === 'function')
1773
                    {
1774
                        prettyPrint();
1775
                    }
1776
1777
                    $prettyPrint.html(
1778
                        prettyPrintOne(
1779
                            Helper.htmlEntities(text), null, true
1780
                        )
1781
                    );
1782
                    // fall through, as the rest is the same
1783
                default: // = 'plaintext'
1784
                    // convert URLs to clickable links
1785
                    Helper.urls2links($plainText);
1786
                    Helper.urls2links($prettyPrint);
1787
1788
                    $prettyPrint.css('white-space', 'pre-wrap');
1789
                    $prettyPrint.css('word-break', 'normal');
1790
                    $prettyPrint.removeClass('prettyprint');
1791
            }
1792
        }
1793
1794
        /**
1795
         * displays the paste
1796
         *
1797
         * @name   PasteViewer.showPaste
1798
         * @private
1799
         * @function
1800
         */
1801
        function showPaste()
1802
        {
1803
            // instead of "nothing" better display a placeholder
1804
            if (text === '') {
1805
                $placeholder.removeClass('hidden')
1806
                return;
1807
            }
1808
            // otherwise hide the placeholder
1809
            $placeholder.addClass('hidden')
1810
1811
            switch (format) {
1812
                case 'markdown':
1813
                    $plainText.removeClass('hidden');
1814
                    $prettyMessage.addClass('hidden');
1815
                    break;
1816
                default:
1817
                    $plainText.addClass('hidden');
1818
                    $prettyMessage.removeClass('hidden');
1819
                    break;
1820
            }
1821
        }
1822
1823
        /**
1824
         * sets the format in which the text is shown
1825
         *
1826
         * @name   PasteViewer.setFormat
1827
         * @function
1828
         * @param {string} newFormat the the new format
1829
         */
1830
        me.setFormat = function(newFormat)
1831
        {
1832
            // skip if there is no update
1833
            if (format === newFormat) {
1834
                return;
1835
            }
1836
1837
            // needs to update display too, if from or to Markdown is switched
1838
            if (format === 'markdown' || newFormat === 'markdown') {
1839
                isDisplayed = false;
1840
            }
1841
1842
            format = newFormat;
1843
            isChanged = true;
1844
        }
1845
1846
        /**
1847
         * returns the current format
1848
         *
1849
         * @name   PasteViewer.getFormat
1850
         * @function
1851
         * @return {string}
1852
         */
1853
        me.getFormat = function()
1854
        {
1855
            return format;
1856
        }
1857
1858
        /**
1859
         * returns whether the current view is pretty printed
1860
         *
1861
         * @name   PasteViewer.isPrettyPrinted
1862
         * @function
1863
         * @return {bool}
1864
         */
1865
        me.isPrettyPrinted = function()
1866
        {
1867
            return $prettyPrint.hasClass('prettyprinted');
1868
        }
1869
1870
        /**
1871
         * sets the text to show
1872
         *
1873
         * @name   PasteViewer.setText
1874
         * @function
1875
         * @param {string} newText the text to show
1876
         */
1877
        me.setText = function(newText)
1878
        {
1879
            if (text !== newText) {
1880
                text = newText;
1881
                isChanged = true;
1882
            }
1883
        }
1884
1885
        /**
1886
         * gets the current cached text
1887
         *
1888
         * @name   PasteViewer.getText
1889
         * @function
1890
         * @return {string}
1891
         */
1892
        me.getText = function()
1893
        {
1894
            return text;
1895
        }
1896
1897
        /**
1898
         * show/update the parsed text (preview)
1899
         *
1900
         * @name   PasteViewer.run
1901
         * @function
1902
         */
1903
        me.run = function()
1904
        {
1905
            if (isChanged) {
1906
                parsePaste();
1907
                isChanged = false;
1908
            }
1909
1910
            if (!isDisplayed) {
1911
                showPaste();
1912
                isDisplayed = true;
1913
            }
1914
        }
1915
1916
        /**
1917
         * hide parsed text (preview)
1918
         *
1919
         * @name   PasteViewer.hide
1920
         * @function
1921
         */
1922
        me.hide = function()
1923
        {
1924
            if (!isDisplayed) {
1925
                console.warn('PasteViewer was called to hide the parsed view, but it is already hidden.');
1926
            }
1927
1928
            $plainText.addClass('hidden');
1929
            $prettyMessage.addClass('hidden');
1930
            $placeholder.addClass('hidden');
1931
1932
            isDisplayed = false;
1933
        }
1934
1935
        /**
1936
         * init status manager
1937
         *
1938
         * preloads jQuery elements
1939
         *
1940
         * @name   PasteViewer.init
1941
         * @function
1942
         */
1943
        me.init = function()
1944
        {
1945
            $placeholder = $('#placeholder');
1946
            $plainText = $('#plaintext');
1947
            $prettyMessage = $('#prettymessage');
1948
            $prettyPrint = $('#prettyprint');
1949
1950
            // check requirements
1951
            if (typeof prettyPrintOne !== 'function') {
1952
                Alert.showError([
1953
                    'The library %s is not available. This may cause display errors.',
1954
                    'pretty print'
1955
                ]);
1956
            }
1957
            if (typeof showdown !== 'object') {
1958
                Alert.showError([
1959
                    'The library %s is not available. This may cause display errors.',
1960
                    'showdown'
1961
                ]);
1962
            }
1963
1964
            // get default option from template/HTML or fall back to set value
1965
            format = Model.getFormatDefault() || format;
1966
        }
1967
1968
        return me;
1969
    })();
1970
1971
    /**
1972
     * (view) Show attachment and preview if possible
1973
     *
1974
     * @name   AttachmentViewer
1975
     * @param  {object} window
1976
     * @param  {object} document
1977
     * @class
1978
     */
1979
    var AttachmentViewer = (function (window, document) {
1980
        var me = {};
1981
1982
        var $attachmentLink,
1983
            $attachmentPreview,
1984
            $attachment;
1985
1986
        var attachmentHasPreview = false;
1987
1988
        /**
1989
         * sets the attachment but does not yet show it
1990
         *
1991
         * @name   AttachmentViewer.setAttachment
1992
         * @function
1993
         * @param {string} attachmentData - base64-encoded data of file
1994
         * @param {string} fileName - optional, file name
1995
         */
1996
        me.setAttachment = function(attachmentData, fileName)
1997
        {
1998
            var imagePrefix = 'data:image/';
1999
2000
            $attachmentLink.attr('href', attachmentData);
2001
            if (typeof fileName !== 'undefined') {
2002
                $attachmentLink.attr('download', fileName);
2003
            }
2004
2005
            // if the attachment is an image, display it
2006
            if (attachmentData.substring(0, imagePrefix.length) === imagePrefix) {
2007
                $attachmentPreview.html(
2008
                    $(document.createElement('img'))
2009
                        .attr('src', attachmentData)
2010
                        .attr('class', 'img-thumbnail')
2011
                );
2012
                attachmentHasPreview = true;
2013
            }
2014
        }
2015
2016
        /**
2017
         * displays the attachment
2018
         *
2019
         * @name AttachmentViewer.showAttachment
2020
         * @function
2021
         */
2022
        me.showAttachment = function()
2023
        {
2024
            $attachment.removeClass('hidden');
2025
2026
            if (attachmentHasPreview) {
2027
                $attachmentPreview.removeClass('hidden');
2028
            }
2029
        }
2030
2031
        /**
2032
         * removes the attachment
2033
         *
2034
         * This automatically hides the attachment containers to, to
2035
         * prevent an inconsistent display.
2036
         *
2037
         * @name AttachmentViewer.removeAttachment
2038
         * @function
2039
         */
2040
        me.removeAttachment = function()
2041
        {
2042
            me.hideAttachment();
2043
            me.hideAttachmentPreview();
2044
            $attachmentLink.prop('href', '');
2045
            $attachmentLink.prop('download', '');
2046
            $attachmentPreview.html('');
2047
        }
2048
2049
        /**
2050
         * hides the attachment
2051
         *
2052
         * This will not hide the preview (see AttachmentViewer.hideAttachmentPreview
2053
         * for that) nor will it hide the attachment link if it was moved somewhere
2054
         * else (see AttachmentViewer.moveAttachmentTo).
2055
         *
2056
         * @name AttachmentViewer.hideAttachment
2057
         * @function
2058
         */
2059
        me.hideAttachment = function()
2060
        {
2061
            $attachment.addClass('hidden');
2062
        }
2063
2064
        /**
2065
         * hides the attachment preview
2066
         *
2067
         * @name AttachmentViewer.hideAttachmentPreview
2068
         * @function
2069
         */
2070
        me.hideAttachmentPreview = function()
2071
        {
2072
            $attachmentPreview.addClass('hidden');
2073
        }
2074
2075
        /**
2076
         * checks if there is an attachment
2077
         *
2078
         * @name   AttachmentViewer.hasAttachment
2079
         * @function
2080
         */
2081
        me.hasAttachment = function()
2082
        {
2083
            var link = $attachmentLink.prop('href');
2084
            return (typeof link !== 'undefined' && link !== '')
2085
        }
2086
2087
        /**
2088
         * return the attachment
2089
         *
2090
         * @name   AttachmentViewer.getAttachment
2091
         * @function
2092
         * @returns {array}
2093
         */
2094
        me.getAttachment = function()
2095
        {
2096
            return [
2097
                $attachmentLink.prop('href'),
2098
                $attachmentLink.prop('download')
2099
            ];
2100
        }
2101
2102
        /**
2103
         * moves the attachment link to another element
2104
         *
2105
         * It is advisable to hide the attachment afterwards (AttachmentViewer.hideAttachment)
2106
         *
2107
         * @name   AttachmentViewer.moveAttachmentTo
2108
         * @function
2109
         * @param {jQuery} $element - the wrapper/container element where this should be moved to
2110
         * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated
2111
         */
2112
        me.moveAttachmentTo = function($element, label)
2113
        {
2114
            // move elemement to new place
2115
            $attachmentLink.appendTo($element);
2116
2117
            // update text
2118
            I18n._($attachmentLink, label, $attachmentLink.attr('download'));
2119
        }
2120
2121
        /**
2122
         * initiate
2123
         *
2124
         * preloads jQuery elements
2125
         *
2126
         * @name   AttachmentViewer.init
2127
         * @function
2128
         */
2129
        me.init = function()
2130
        {
2131
            $attachment = $('#attachment');
2132
            $attachmentLink = $('#attachment a');
2133
            $attachmentPreview = $('#attachmentPreview');
2134
        }
2135
2136
        return me;
2137
    })(window, document);
2138
2139
    /**
2140
     * (view) Shows discussion thread and handles replies
2141
     *
2142
     * @name   DiscussionViewer
2143
     * @param  {object} window
2144
     * @param  {object} document
2145
     * @class
2146
     */
2147
    var DiscussionViewer = (function (window, document) {
2148
        var me = {};
2149
2150
        var $commentTail,
2151
            $discussion,
2152
            $reply,
2153
            $replyMessage,
2154
            $replyNickname,
2155
            $replyStatus,
2156
            $commentContainer;
2157
2158
        var replyCommentId;
2159
2160
        /**
2161
         * initializes the templates
2162
         *
2163
         * @name   DiscussionViewer.initTemplates
2164
         * @private
2165
         * @function
2166
         */
2167
        function initTemplates()
2168
        {
2169
            $reply = Model.getTemplate('reply');
2170
            $replyMessage = $reply.find('#replymessage');
2171
            $replyNickname = $reply.find('#nickname');
2172
            $replyStatus = $reply.find('#replystatus');
2173
2174
            // cache jQuery elements
2175
            $commentTail = Model.getTemplate('commenttail');
2176
        }
2177
2178
        /**
2179
         * open the comment entry when clicking the "Reply" button of a comment
2180
         *
2181
         * @name   DiscussionViewer.openReply
2182
         * @private
2183
         * @function
2184
         * @param  {Event} event
2185
         */
2186
        function openReply(event)
2187
        {
2188
            var $source = $(event.target);
2189
2190
            // clear input
2191
            $replyMessage.val('');
2192
            $replyNickname.val('');
2193
2194
            // get comment id from source element
2195
            replyCommentId = $source.parent().prop('id').split('_')[1];
2196
2197
            // move to correct position
2198
            $source.after($reply);
2199
2200
            // show
2201
            $reply.removeClass('hidden');
2202
            $replyMessage.focus();
2203
2204
            event.preventDefault();
2205
        }
2206
2207
        /**
2208
         * custom handler for displaying notifications in own status message area
2209
         *
2210
         * @name   DiscussionViewer.handleNotification
2211
         * @function
2212
         * @param  {string} alertType
2213
         * @param  {jQuery} $element
2214
         * @param  {string|array} args
2215
         * @param  {string|null} icon
2216
         * @return {bool|jQuery}
2217
         */
2218
        me.handleNotification = function(alertType, $element, args, icon)
2219
        {
2220
            // ignore loading messages
2221
            if (alertType === 'loading') {
2222
                return false;
2223
            }
2224
2225
            if (alertType === 'danger') {
2226
                $replyStatus.removeClass('alert-info');
2227
                $replyStatus.addClass('alert-danger');
2228
                $replyStatus.find(':first').removeClass('glyphicon-alert');
2229
                $replyStatus.find(':first').addClass('glyphicon-info-sign');
2230
            } else {
2231
                $replyStatus.removeClass('alert-danger');
2232
                $replyStatus.addClass('alert-info');
2233
                $replyStatus.find(':first').removeClass('glyphicon-info-sign');
2234
                $replyStatus.find(':first').addClass('glyphicon-alert');
2235
            }
2236
2237
            return $replyStatus;
2238
        }
2239
2240
        /**
2241
         * adds another comment
2242
         *
2243
         * @name   DiscussionViewer.addComment
2244
         * @function
2245
         * @param {object} comment
2246
         * @param {string} commentText
2247
         * @param {jQuery} $place      - optional, tries to find the best position otherwise
2248
         */
2249
        me.addComment = function(comment, commentText, nickname, $place)
2250
        {
2251
            if (typeof $place === 'undefined') {
2252
                // starting point (default value/fallback)
2253
                $place = $commentContainer;
2254
2255
                // if parent comment exists
2256
                var $parentComment = $('#comment_' + comment.parentid);
2257
                if ($parentComment.length) {
2258
                    // use parent as position for noew comment, so it shifted
2259
                    // to the right
2260
                    $place = $parentComment;
2261
                }
2262
            }
2263
            if (commentText === '') {
2264
                commentText = 'comment decryption failed';
2265
            }
2266
2267
            // create new comment based on template
2268
            var $commentEntry = Model.getTemplate('comment');
2269
            $commentEntry.prop('id', 'comment_' + comment.id);
2270
            var $commentEntryData = $commentEntry.find('div.commentdata');
2271
2272
            // set & parse text
2273
            Helper.setElementText($commentEntryData, commentText);
2274
            Helper.urls2links($commentEntryData);
2275
2276
            // set nickname
2277
            if (nickname.length > 0) {
2278
                $commentEntry.find('span.nickname').text(nickname);
2279
            } else {
2280
                $commentEntry.find('span.nickname').html('<i></i>');
2281
                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
2282
            }
2283
2284
            // set date
2285
            $commentEntry.find('span.commentdate')
2286
                      .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
2287
                      .attr('title', 'CommentID: ' + comment.id);
2288
2289
            // if an avatar is available, display it
2290
            if (comment.meta.vizhash) {
2291
                $commentEntry.find('span.nickname')
2292
                             .before(
2293
                                '<img src="' + comment.meta.vizhash + '" class="vizhash" /> '
2294
                             );
2295
                $(document).on('languageLoaded', function () {
2296
                    $commentEntry.find('img.vizhash')
2297
                                 .prop('title', I18n._('Avatar generated from IP address'));
2298
                });
2299
            }
2300
2301
            // finally append comment
2302
            $place.append($commentEntry);
2303
        }
2304
2305
        /**
2306
         * finishes the discussion area after last comment
2307
         *
2308
         * @name   DiscussionViewer.finishDiscussion
2309
         * @function
2310
         */
2311
        me.finishDiscussion = function()
2312
        {
2313
            // add 'add new comment' area
2314
            $commentContainer.append($commentTail);
2315
2316
            // show discussions
2317
            $discussion.removeClass('hidden');
2318
        }
2319
2320
        /**
2321
         * shows the discussion area
2322
         *
2323
         * @name   DiscussionViewer.showDiscussion
2324
         * @function
2325
         */
2326
        me.showDiscussion = function()
2327
        {
2328
            $discussion.removeClass('hidden');
2329
        }
2330
2331
        /**
2332
         * removes the old discussion and prepares everything for creating a new
2333
         * one.
2334
         *
2335
         * @name   DiscussionViewer.prepareNewDisucssion
2336
         * @function
2337
         */
2338
        me.prepareNewDisucssion = function()
2339
        {
2340
            $commentContainer.html('');
2341
            $discussion.addClass('hidden');
2342
2343
            // (re-)init templates
2344
            initTemplates();
2345
        }
2346
2347
        /**
2348
         * returns the user put into the reply form
2349
         *
2350
         * @name   DiscussionViewer.getReplyData
2351
         * @function
2352
         * @return {array}
2353
         */
2354
        me.getReplyData = function()
2355
        {
2356
            return [
2357
                $replyMessage.val(),
2358
                $replyNickname.val()
2359
            ];
2360
        }
2361
2362
        /**
2363
         * highlights a specific comment and scrolls to it if necessary
2364
         *
2365
         * @name   DiscussionViewer.highlightComment
2366
         * @function
2367
         * @param {string} commentId
2368
         * @param {bool} fadeOut - whether to fade out the comment
2369
         */
2370
        me.highlightComment = function(commentId, fadeOut)
2371
        {
2372
            var $comment = $('#comment_' + commentId);
2373
            // in case comment does not exist, cancel
2374
            if ($comment.length === 0) {
2375
                return;
2376
            }
2377
2378
            var highlightComment = function () {
2379
                $comment.addClass('highlight');
2380
                if (fadeOut === true) {
2381
                    setTimeout(function () {
2382
                        $comment.removeClass('highlight');
2383
                    }, 300);
2384
                }
2385
            }
2386
2387
            if (UiHelper.isVisible($comment)) {
2388
                return highlightComment();
2389
            }
2390
2391
            UiHelper.scrollTo($comment, 100, 'swing', highlightComment);
2392
        }
2393
2394
        /**
2395
         * returns the id of the parent comment the user is replying to
2396
         *
2397
         * @name   DiscussionViewer.getReplyCommentId
2398
         * @function
2399
         * @return {int|undefined}
2400
         */
2401
        me.getReplyCommentId = function()
2402
        {
2403
            return replyCommentId;
2404
        }
2405
2406
        /**
2407
         * initiate
2408
         *
2409
         * preloads jQuery elements
2410
         *
2411
         * @name   DiscussionViewer.init
2412
         * @function
2413
         */
2414
        me.init = function()
2415
        {
2416
            // bind events to templates (so they are later cloned)
2417
            $('#commenttailtemplate, #commenttemplate').find('button').on('click', openReply);
2418
            $('#replytemplate').find('button').on('click', PasteEncrypter.sendComment);
2419
2420
            $commentContainer = $('#commentcontainer');
2421
            $discussion = $('#discussion');
2422
        }
2423
2424
        return me;
2425
    })(window, document);
2426
2427
    /**
2428
     * Manage top (navigation) bar
2429
     *
2430
     * @name   TopNav
2431
     * @param  {object} window
2432
     * @param  {object} document
2433
     * @class
2434
     */
2435
    var TopNav = (function (window, document) {
2436
        var me = {};
2437
2438
        var createButtonsDisplayed = false;
2439
        var viewButtonsDisplayed = false;
2440
2441
        var $attach,
2442
            $burnAfterReading,
2443
            $burnAfterReadingOption,
2444
            $cloneButton,
2445
            $customAttachment,
2446
            $expiration,
2447
            $fileRemoveButton,
2448
            $fileWrap,
2449
            $formatter,
2450
            $newButton,
2451
            $openDiscussion,
2452
            $openDiscussionOption,
2453
            $password,
2454
            $passwordInput,
2455
            $rawTextButton,
2456
            $sendButton;
2457
2458
        var pasteExpiration = '1week';
2459
2460
        /**
2461
         * set the expiration on bootstrap templates in dropdown
2462
         *
2463
         * @name   TopNav.updateExpiration
2464
         * @private
2465
         * @function
2466
         * @param  {Event} event
2467
         */
2468
        function updateExpiration(event)
2469
        {
2470
            // get selected option
2471
            var target = $(event.target);
2472
2473
            // update dropdown display and save new expiration time
2474
            $('#pasteExpirationDisplay').text(target.text());
2475
            pasteExpiration = target.data('expiration');
2476
2477
            event.preventDefault();
2478
        }
2479
2480
        /**
2481
         * set the format on bootstrap templates in dropdown
2482
         *
2483
         * @name   TopNav.updateFormat
2484
         * @private
2485
         * @function
2486
         * @param  {Event} event
2487
         */
2488
        function updateFormat(event)
2489
        {
2490
            // get selected option
2491
            var $target = $(event.target);
2492
2493
            // update dropdown display and save new format
2494
            var newFormat = $target.data('format');
2495
            $('#pasteFormatterDisplay').text($target.text());
2496
            PasteViewer.setFormat(newFormat);
2497
2498
            // update preview
2499
            if (Editor.isPreview()) {
2500
                PasteViewer.run();
2501
            }
2502
2503
            event.preventDefault();
2504
        }
2505
2506
        /**
2507
         * when "burn after reading" is checked, disable discussion
2508
         *
2509
         * @name   TopNav.changeBurnAfterReading
2510
         * @private
2511
         * @function
2512
         */
2513
        function changeBurnAfterReading()
2514
        {
2515
            if ($burnAfterReading.is(':checked')) {
2516
                $openDiscussionOption.addClass('buttondisabled');
2517
                $openDiscussion.prop('checked', false);
2518
2519
                // if button is actually disabled, force-enable it and uncheck other button
2520
                $burnAfterReadingOption.removeClass('buttondisabled');
2521
            } else {
2522
                $openDiscussionOption.removeClass('buttondisabled');
2523
            }
2524
        }
2525
2526
        /**
2527
         * when discussion is checked, disable "burn after reading"
2528
         *
2529
         * @name   TopNav.changeOpenDiscussion
2530
         * @private
2531
         * @function
2532
         */
2533
        function changeOpenDiscussion()
2534
        {
2535
            if ($openDiscussion.is(':checked')) {
2536
                $burnAfterReadingOption.addClass('buttondisabled');
2537
                $burnAfterReading.prop('checked', false);
2538
2539
                // if button is actually disabled, force-enable it and uncheck other button
2540
                $openDiscussionOption.removeClass('buttondisabled');
2541
            } else {
2542
                $burnAfterReadingOption.removeClass('buttondisabled');
2543
            }
2544
        }
2545
2546
        /**
2547
         * return raw text
2548
         *
2549
         * @name   TopNav.rawText
2550
         * @private
2551
         * @function
2552
         * @param  {Event} event
2553
         */
2554
        function rawText(event)
2555
        {
2556
            TopNav.hideAllButtons();
2557
            Alert.showLoading('Showing raw text…', 0, 'time');
2558
            var paste = PasteViewer.getText();
2559
2560
            // push a new state to allow back navigation with browser back button
2561
            history.pushState(
2562
                {type: 'raw'},
2563
                document.title,
2564
                // recreate paste URL
2565
                Helper.baseUri() + '?' + Model.getPasteId() + '#' +
2566
                Model.getPasteKey()
2567
            );
2568
2569
            // we use text/html instead of text/plain to avoid a bug when
2570
            // reloading the raw text view (it reverts to type text/html)
2571
            var $head = $('head').children().not('noscript, script, link[type="text/css"]');
2572
            var newDoc = document.open('text/html', 'replace');
2573
            newDoc.write('<!DOCTYPE html><html><head>');
2574
            for (var i = 0; i < $head.length; i++) {
2575
                newDoc.write($head[i].outerHTML);
2576
            }
2577
            newDoc.write('</head><body><pre>' + Helper.htmlEntities(paste) + '</pre></body></html>');
2578
            newDoc.close();
2579
        }
2580
2581
        /**
2582
         * saves the language in a cookie and reloads the page
2583
         *
2584
         * @name   TopNav.setLanguage
2585
         * @private
2586
         * @function
2587
         * @param  {Event} event
2588
         */
2589
        function setLanguage(event)
2590
        {
2591
            document.cookie = 'lang=' + $(event.target).data('lang');
2592
            UiHelper.reloadHome();
2593
        }
2594
2595
        /**
2596
         * hides all messages and creates a new paste
2597
         *
2598
         * @name   TopNav.clickNewPaste
2599
         * @private
2600
         * @function
2601
         * @param  {Event} event
2602
         */
2603
        function clickNewPaste(event)
2604
        {
2605
            Controller.hideStatusMessages();
2606
            Controller.newPaste();
2607
        }
2608
2609
        /**
2610
         * removes the existing attachment
2611
         *
2612
         * @name   TopNav.removeAttachment
2613
         * @private
2614
         * @function
2615
         * @param  {Event} event
2616
         */
2617
        function removeAttachment(event)
2618
        {
2619
            // if custom attachment is used, remove it first
2620
            if (!$customAttachment.hasClass('hidden')) {
2621
                AttachmentViewer.removeAttachment();
2622
                $customAttachment.addClass('hidden');
2623
                $fileWrap.removeClass('hidden');
2624
            }
2625
2626
            // our up-to-date jQuery can handle it :)
2627
            $fileWrap.find('input').val('');
2628
2629
            // pevent '#' from appearing in the URL
2630
            event.preventDefault();
2631
        }
2632
2633
        /**
2634
         * Shows all elements belonging to viwing an existing pastes
2635
         *
2636
         * @name   TopNav.showViewButtons
2637
         * @function
2638
         */
2639
        me.showViewButtons = function()
2640
        {
2641
            if (viewButtonsDisplayed) {
2642
                console.log('showViewButtons: view buttons are already displayed');
2643
                return;
2644
            }
2645
2646
            $newButton.removeClass('hidden');
2647
            $cloneButton.removeClass('hidden');
2648
            $rawTextButton.removeClass('hidden');
2649
2650
            viewButtonsDisplayed = true;
2651
        }
2652
2653
        /**
2654
         * Hides all elements belonging to existing pastes
2655
         *
2656
         * @name   TopNav.hideViewButtons
2657
         * @function
2658
         */
2659
        me.hideViewButtons = function()
2660
        {
2661
            if (!viewButtonsDisplayed) {
2662
                console.log('hideViewButtons: view buttons are already hidden');
2663
                return;
2664
            }
2665
2666
            $newButton.addClass('hidden');
2667
            $cloneButton.addClass('hidden');
2668
            $rawTextButton.addClass('hidden');
2669
2670
            viewButtonsDisplayed = false;
2671
        }
2672
2673
        /**
2674
         * Hides all elements belonging to existing pastes
2675
         *
2676
         * @name   TopNav.hideAllButtons
2677
         * @function
2678
         */
2679
        me.hideAllButtons = function()
2680
        {
2681
            me.hideViewButtons();
2682
            me.hideCreateButtons();
2683
        }
2684
2685
        /**
2686
         * shows all elements needed when creating a new paste
2687
         *
2688
         * @name   TopNav.showCreateButtons
2689
         * @function
2690
         */
2691
        me.showCreateButtons = function()
2692
        {
2693
            if (createButtonsDisplayed) {
2694
                console.log('showCreateButtons: create buttons are already displayed');
2695
                return;
2696
            }
2697
2698
            $sendButton.removeClass('hidden');
2699
            $expiration.removeClass('hidden');
2700
            $formatter.removeClass('hidden');
2701
            $burnAfterReadingOption.removeClass('hidden');
2702
            $openDiscussionOption.removeClass('hidden');
2703
            $newButton.removeClass('hidden');
2704
            $password.removeClass('hidden');
2705
            $attach.removeClass('hidden');
2706
2707
            createButtonsDisplayed = true;
2708
        }
2709
2710
        /**
2711
         * shows all elements needed when creating a new paste
2712
         *
2713
         * @name   TopNav.hideCreateButtons
2714
         * @function
2715
         */
2716
        me.hideCreateButtons = function()
2717
        {
2718
            if (!createButtonsDisplayed) {
2719
                console.log('hideCreateButtons: create buttons are already hidden');
2720
                return;
2721
            }
2722
2723
            $newButton.addClass('hidden');
2724
            $sendButton.addClass('hidden');
2725
            $expiration.addClass('hidden');
2726
            $formatter.addClass('hidden');
2727
            $burnAfterReadingOption.addClass('hidden');
2728
            $openDiscussionOption.addClass('hidden');
2729
            $password.addClass('hidden');
2730
            $attach.addClass('hidden');
2731
2732
            createButtonsDisplayed = false;
2733
        }
2734
2735
        /**
2736
         * only shows the "new paste" button
2737
         *
2738
         * @name   TopNav.showNewPasteButton
2739
         * @function
2740
         */
2741
        me.showNewPasteButton = function()
2742
        {
2743
            $newButton.removeClass('hidden');
2744
        }
2745
2746
        /**
2747
         * only hides the clone button
2748
         *
2749
         * @name   TopNav.hideCloneButton
2750
         * @function
2751
         */
2752
        me.hideCloneButton = function()
2753
        {
2754
            $cloneButton.addClass('hidden');
2755
        }
2756
2757
        /**
2758
         * only hides the raw text button
2759
         *
2760
         * @name   TopNav.hideRawButton
2761
         * @function
2762
         */
2763
        me.hideRawButton = function()
2764
        {
2765
            $rawTextButton.addClass('hidden');
2766
        }
2767
2768
        /**
2769
         * hides the file selector in attachment
2770
         *
2771
         * @name   TopNav.hideFileSelector
2772
         * @function
2773
         */
2774
        me.hideFileSelector = function()
2775
        {
2776
            $fileWrap.addClass('hidden');
2777
        }
2778
2779
2780
        /**
2781
         * shows the custom attachment
2782
         *
2783
         * @name   TopNav.showCustomAttachment
2784
         * @function
2785
         */
2786
        me.showCustomAttachment = function()
2787
        {
2788
            $customAttachment.removeClass('hidden');
2789
        }
2790
2791
        /**
2792
         * collapses the navigation bar if nedded
2793
         *
2794
         * @name   TopNav.collapseBar
2795
         * @function
2796
         */
2797
        me.collapseBar = function()
2798
        {
2799
            var $bar = $('.navbar-toggle');
2800
2801
            // check if bar is expanded
2802
            if ($bar.hasClass('collapse in')) {
2803
                // if so, toggle it
2804
                $bar.click();
2805
            }
2806
        }
2807
2808
        /**
2809
         * returns the currently set expiration time
2810
         *
2811
         * @name   TopNav.getExpiration
2812
         * @function
2813
         * @return {int}
2814
         */
2815
        me.getExpiration = function()
2816
        {
2817
            return pasteExpiration;
2818
        }
2819
2820
        /**
2821
         * returns the currently selected file(s)
2822
         *
2823
         * @name   TopNav.getFileList
2824
         * @function
2825
         * @return {FileList|null}
2826
         */
2827
        me.getFileList = function()
2828
        {
2829
            var $file = $('#file');
2830
2831
            // if no file given, return null
2832
            if (!$file.length || !$file[0].files.length) {
2833
                return null;
2834
            }
2835
            // @TODO is this really necessary
2836
            if (!($file[0].files && $file[0].files[0])) {
2837
                return null;
2838
            }
2839
2840
            return $file[0].files;
2841
        }
2842
2843
        /**
2844
         * returns the state of the burn after reading checkbox
2845
         *
2846
         * @name   TopNav.getExpiration
2847
         * @function
2848
         * @return {bool}
2849
         */
2850
        me.getBurnAfterReading = function()
2851
        {
2852
            return $burnAfterReading.is(':checked');
2853
        }
2854
2855
        /**
2856
         * returns the state of the discussion checkbox
2857
         *
2858
         * @name   TopNav.getOpenDiscussion
2859
         * @function
2860
         * @return {bool}
2861
         */
2862
        me.getOpenDiscussion = function()
2863
        {
2864
            return $openDiscussion.is(':checked');
2865
        }
2866
2867
        /**
2868
         * returns the entered password
2869
         *
2870
         * @name   TopNav.getPassword
2871
         * @function
2872
         * @return {string}
2873
         */
2874
        me.getPassword = function()
2875
        {
2876
            return $passwordInput.val();
2877
        }
2878
2879
        /**
2880
         * returns the element where custom attachments can be placed
2881
         *
2882
         * Used by AttachmentViewer when an attachment is cloned here.
2883
         *
2884
         * @name   TopNav.getCustomAttachment
2885
         * @function
2886
         * @return {jQuery}
2887
         */
2888
        me.getCustomAttachment = function()
2889
        {
2890
            return $customAttachment;
2891
        }
2892
2893
        /**
2894
         * init navigation manager
2895
         *
2896
         * preloads jQuery elements
2897
         *
2898
         * @name   TopNav.init
2899
         * @function
2900
         */
2901
        me.init = function()
2902
        {
2903
            $attach = $('#attach');
2904
            $burnAfterReading = $('#burnafterreading');
2905
            $burnAfterReadingOption = $('#burnafterreadingoption');
2906
            $cloneButton = $('#clonebutton');
2907
            $customAttachment = $('#customattachment');
2908
            $expiration = $('#expiration');
2909
            $fileRemoveButton = $('#fileremovebutton');
2910
            $fileWrap = $('#filewrap');
2911
            $formatter = $('#formatter');
2912
            $newButton = $('#newbutton');
2913
            $openDiscussion = $('#opendiscussion');
2914
            $openDiscussionOption = $('#opendiscussionoption');
2915
            $password = $('#password');
2916
            $passwordInput = $('#passwordinput');
2917
            $rawTextButton = $('#rawtextbutton');
2918
            $sendButton = $('#sendbutton');
2919
2920
            // bootstrap template drop down
2921
            $('#language ul.dropdown-menu li a').click(setLanguage);
2922
            // page template drop down
2923
            $('#language select option').click(setLanguage);
2924
2925
            // bind events
2926
            $burnAfterReading.change(changeBurnAfterReading);
2927
            $openDiscussionOption.change(changeOpenDiscussion);
2928
            $newButton.click(clickNewPaste);
2929
            $sendButton.click(PasteEncrypter.sendPaste);
2930
            $cloneButton.click(Controller.clonePaste);
2931
            $rawTextButton.click(rawText);
2932
            $fileRemoveButton.click(removeAttachment);
2933
2934
            // bootstrap template drop downs
2935
            $('ul.dropdown-menu li a', $('#expiration').parent()).click(updateExpiration);
2936
            $('ul.dropdown-menu li a', $('#formatter').parent()).click(updateFormat);
2937
2938
            // initiate default state of checkboxes
2939
            changeBurnAfterReading();
2940
            changeOpenDiscussion();
2941
2942
            // get default value from template or fall back to set value
2943
            pasteExpiration = Model.getExpirationDefault() || pasteExpiration;
2944
        }
2945
2946
        return me;
2947
    })(window, document);
2948
2949
    /**
2950
     * Responsible for AJAX requests, transparently handles encryption…
2951
     *
2952
     * @name   Uploader
2953
     * @class
2954
     */
2955
    var Uploader = (function () {
2956
        var me = {};
2957
2958
        var successFunc = null,
2959
            failureFunc = null,
2960
            url,
2961
            data,
2962
            symmetricKey,
2963
            password;
2964
2965
        /**
2966
         * public variable ('constant') for errors to prevent magic numbers
2967
         *
2968
         * @name   Uploader.error
2969
         * @readonly
2970
         * @enum   {Object}
2971
         */
2972
        me.error = {
2973
            okay: 0,
2974
            custom: 1,
2975
            unknown: 2,
2976
            serverError: 3
2977
        };
2978
2979
        /**
2980
         * ajaxHeaders to send in AJAX requests
2981
         *
2982
         * @name   Uploader.ajaxHeaders
2983
         * @private
2984
         * @readonly
2985
         * @enum   {Object}
2986
         */
2987
        var ajaxHeaders = {'X-Requested-With': 'JSONHttpRequest'};
2988
2989
        /**
2990
         * called after successful upload
2991
         *
2992
         * @name   Uploader.checkCryptParameters
2993
         * @private
2994
         * @function
2995
         * @throws {string}
2996
         */
2997
        function checkCryptParameters()
2998
        {
2999
            // workaround for this nasty 'bug' in ECMAScript
3000
            // see https://stackoverflow.com/questions/18808226/why-is-typeof-null-object
3001
            var typeOfKey = typeof symmetricKey;
3002
            if (symmetricKey === null) {
3003
                typeOfKey = 'null';
3004
            }
3005
3006
            // in case of missing preparation, throw error
3007
            switch (typeOfKey) {
3008
                case 'string':
3009
                    // already set, all right
3010
                    return;
3011
                case 'null':
3012
                    // needs to be generated auto-generate
3013
                    symmetricKey = CryptTool.getSymmetricKey();
3014
                    break;
3015
                default:
3016
                    console.error('current invalid symmetricKey:', symmetricKey);
3017
                    throw 'symmetricKey is invalid, probably the module was not prepared';
3018
            }
3019
            // password is optional
3020
        }
3021
3022
        /**
3023
         * called after successful upload
3024
         *
3025
         * @name   Uploader.success
3026
         * @private
3027
         * @function
3028
         * @param {int} status
3029
         * @param {int} result - optional
3030
         */
3031
        function success(status, result)
3032
        {
3033
            // add useful data to result
3034
            result.encryptionKey = symmetricKey;
3035
            result.requestData = data;
3036
3037
            if (successFunc !== null) {
3038
                successFunc(status, result);
3039
            }
3040
        }
3041
3042
        /**
3043
         * called after a upload failure
3044
         *
3045
         * @name   Uploader.fail
3046
         * @private
3047
         * @function
3048
         * @param {int} status - internal code
3049
         * @param {int} result - original error code
3050
         */
3051
        function fail(status, result)
3052
        {
3053
            if (failureFunc !== null) {
3054
                failureFunc(status, result);
3055
            }
3056
        }
3057
3058
        /**
3059
         * actually uploads the data
3060
         *
3061
         * @name   Uploader.run
3062
         * @function
3063
         */
3064
        me.run = function()
3065
        {
3066
            $.ajax({
3067
                type: 'POST',
3068
                url: url,
3069
                data: data,
3070
                dataType: 'json',
3071
                headers: ajaxHeaders,
3072
                success: function(result) {
3073
                    if (result.status === 0) {
3074
                        success(0, result);
3075
                    } else if (result.status === 1) {
3076
                        fail(1, result);
3077
                    } else {
3078
                        fail(2, result);
3079
                    }
3080
                }
3081
            })
3082
            .fail(function(jqXHR, textStatus, errorThrown) {
3083
                console.error(textStatus, errorThrown);
3084
                fail(3, jqXHR);
3085
            });
3086
        }
3087
3088
        /**
3089
         * set success function
3090
         *
3091
         * @name   Uploader.setUrl
3092
         * @function
3093
         * @param {function} newUrl
3094
         */
3095
        me.setUrl = function(newUrl)
3096
        {
3097
            url = newUrl;
3098
        }
3099
3100
        /**
3101
         * sets the password to use (first value) and optionally also the
3102
         * encryption key (not recommend, it is automatically generated).
3103
         *
3104
         * Note: Call this after prepare() as prepare() resets these values.
3105
         *
3106
         * @name   Uploader.setCryptValues
3107
         * @function
3108
         * @param {string} newPassword
3109
         * @param {string} newKey       - optional
3110
         */
3111
        me.setCryptParameters = function(newPassword, newKey)
3112
        {
3113
            password = newPassword;
3114
3115
            if (typeof newKey !== 'undefined') {
3116
                symmetricKey = newKey;
3117
            }
3118
        }
3119
3120
        /**
3121
         * set success function
3122
         *
3123
         * @name   Uploader.setSuccess
3124
         * @function
3125
         * @param {function} func
3126
         */
3127
        me.setSuccess = function(func)
3128
        {
3129
            successFunc = func;
3130
        }
3131
3132
        /**
3133
         * set failure function
3134
         *
3135
         * @name   Uploader.setFailure
3136
         * @function
3137
         * @param {function} func
3138
         */
3139
        me.setFailure = function(func)
3140
        {
3141
            failureFunc = func;
3142
        }
3143
3144
        /**
3145
         * prepares a new upload
3146
         *
3147
         * Call this when doing a new upload to reset any data from potential
3148
         * previous uploads. Must be called before any other method of this
3149
         * module.
3150
         *
3151
         * @name   Uploader.prepare
3152
         * @function
3153
         * @return {object}
3154
         */
3155
        me.prepare = function()
3156
        {
3157
            // entropy should already be checked!
3158
3159
            // reset password
3160
            password = '';
3161
3162
            // reset key, so it a new one is generated when it is used
3163
            symmetricKey = null;
3164
3165
            // reset data
3166
            successFunc = null;
3167
            failureFunc = null;
3168
            url = Helper.baseUri();
3169
            data = {};
3170
        }
3171
3172
        /**
3173
         * encrypts and sets the data
3174
         *
3175
         * @name   Uploader.setData
3176
         * @function
3177
         * @param {string} index
3178
         * @param {mixed} element
3179
         */
3180
        me.setData = function(index, element)
3181
        {
3182
            checkCryptParameters();
3183
            data[index] = CryptTool.cipher(symmetricKey, password, element);
3184
        }
3185
3186
        /**
3187
         * set the additional metadata to send unencrypted
3188
         *
3189
         * @name   Uploader.setUnencryptedData
3190
         * @function
3191
         * @param {string} index
3192
         * @param {mixed} element
3193
         */
3194
        me.setUnencryptedData = function(index, element)
3195
        {
3196
            data[index] = element;
3197
        }
3198
3199
        /**
3200
         * set the additional metadata to send unencrypted passed at once
3201
         *
3202
         * @name   Uploader.setUnencryptedData
3203
         * @function
3204
         * @param {object} newData
3205
         */
3206
        me.setUnencryptedBulkData = function(newData)
3207
        {
3208
            $.extend(data, newData);
3209
        }
3210
3211
        /**
3212
         * Helper, which parses shows a general error message based on the result of the Uploader
3213
         *
3214
         * @name    Uploader.parseUploadError
3215
         * @function
3216
         * @param {int} status
3217
         * @param {object} data
3218
         * @param {string} doThisThing - a human description of the action, which was tried
3219
         * @return {array}
3220
         */
3221
        me.parseUploadError = function(status, data, doThisThing) {
3222
            var errorArray;
3223
3224
            switch (status) {
3225
                case me.error['custom']:
0 ignored issues
show
The variable me seems to be never declared. If this is a global, consider adding a /** global: me */ 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...
3226
                    errorArray = ['Could not ' + doThisThing + ': %s', data.message];
3227
                    break;
3228
                case me.error['unknown']:
3229
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown status')];
3230
                    break;
3231
                case me.error['serverError']:
3232
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('server error or not responding')];
3233
                    break;
3234
                default:
3235
                    errorArray = ['Could not ' + doThisThing + ': %s', I18n._('unknown error')];
3236
                    break;
3237
            }
3238
3239
            return errorArray;
3240
        }
3241
3242
        /**
3243
         * init Uploader
3244
         *
3245
         * @name   Uploader.init
3246
         * @function
3247
         */
3248
        me.init = function()
3249
        {
3250
            // nothing yet
3251
        }
3252
3253
        return me;
3254
    })();
3255
3256
    /**
3257
     * (controller) Responsible for encrypting paste and sending it to server.
3258
     *
3259
     * Does upload, encryption is done transparently by Uploader.
3260
     *
3261
     * @name PasteEncrypter
3262
     * @class
3263
     */
3264
    var PasteEncrypter = (function () {
3265
        var me = {};
3266
3267
        var requirementsChecked = false;
3268
3269
        /**
3270
         * checks whether there is a suitable amount of entrophy
3271
         *
3272
         * @name PasteEncrypter.checkRequirements
3273
         * @private
3274
         * @function
3275
         * @param {function} retryCallback - the callback to execute to retry the upload
3276
         * @return {bool}
3277
         */
3278
        function checkRequirements(retryCallback) {
3279
            // skip double requirement checks
3280
            if (requirementsChecked === true) {
3281
                return true;
3282
            }
3283
3284
            if (!CryptTool.isEntropyReady()) {
3285
                // display a message and wait
3286
                Alert.showStatus('Please move your mouse for more entropy…');
3287
3288
                CryptTool.addEntropySeedListener(retryCallback);
3289
                return false;
3290
            }
3291
3292
            requirementsChecked = true;
3293
3294
            return true;
3295
        }
3296
3297
        /**
3298
         * called after successful paste upload
3299
         *
3300
         * @name PasteEncrypter.showCreatedPaste
3301
         * @private
3302
         * @function
3303
         * @param {int} status
3304
         * @param {object} data
3305
         */
3306
        function showCreatedPaste(status, data) {
3307
            Alert.hideLoading();
3308
3309
            var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey,
3310
                deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
3311
3312
            Alert.hideMessages();
3313
3314
            // show notification
3315
            PasteStatus.createPasteNotification(url, deleteUrl)
3316
3317
            // show new URL in browser bar
3318
            history.pushState({type: 'newpaste'}, document.title, url);
3319
3320
            TopNav.showViewButtons();
3321
            TopNav.hideRawButton();
3322
            Editor.hide();
3323
3324
            // parse and show text
3325
            // (preparation already done in me.sendPaste())
3326
            PasteViewer.run();
3327
        }
3328
3329
        /**
3330
         * called after successful comment upload
3331
         *
3332
         * @name PasteEncrypter.showUploadedComment
3333
         * @private
3334
         * @function
3335
         * @param {int} status
3336
         * @param {object} data
3337
         */
3338
        function showUploadedComment(status, data) {
3339
            // show success message
3340
            // Alert.showStatus('Comment posted.');
3341
3342
            // reload paste
3343
            Controller.refreshPaste(function () {
3344
                // highlight sent comment
3345
                DiscussionViewer.highlightComment(data.id, true);
3346
                // reset error handler
3347
                Alert.setCustomHandler(null);
3348
            });
3349
        }
3350
3351
        /**
3352
         * adds attachments to the Uploader
3353
         *
3354
         * @name PasteEncrypter.encryptAttachments
3355
         * @private
3356
         * @function
3357
         * @param {File|null|undefined} file - optional, falls back to cloned attachment
3358
         * @param {function} callback - excuted when action is successful
3359
         */
3360
        function encryptAttachments(file, callback) {
3361
            if (typeof file !== 'undefined' && file !== null) {
3362
                // check file reader requirements for upload
3363
                if (typeof FileReader === 'undefined') {
3364
                    Alert.showError('Your browser does not support uploading encrypted files. Please use a newer browser.');
3365
                    // cancels process as it does not execute callback
3366
                    return;
3367
                }
3368
3369
                var reader = new FileReader();
3370
3371
                // closure to capture the file information
3372
                reader.onload = function(event) {
3373
                    Uploader.setData('attachment', event.target.result);
3374
                    Uploader.setData('attachmentname', file.name);
3375
3376
                    // run callback
3377
                    return callback();
3378
                }
3379
3380
                // actually read first file
3381
                reader.readAsDataURL(file);
3382
            } else if (AttachmentViewer.hasAttachment()) {
3383
                // fall back to cloned part
3384
                var attachment = AttachmentViewer.getAttachment();
3385
3386
                Uploader.setData('attachment', attachment[0]);
3387
                Uploader.setData('attachmentname', attachment[1]);
3388
                return callback();
3389
            } else {
3390
                // if there are no attachments, this is of course still successful
3391
                return callback();
3392
            }
3393
        }
3394
3395
        /**
3396
         * send a reply in a discussion
3397
         *
3398
         * @name   PasteEncrypter.sendComment
3399
         * @function
3400
         */
3401
        me.sendComment = function()
3402
        {
3403
            Alert.hideMessages();
3404
            Alert.setCustomHandler(DiscussionViewer.handleNotification);
3405
3406
            // UI loading state
3407
            TopNav.hideAllButtons();
3408
            Alert.showLoading('Sending comment…', 0, 'cloud-upload');
3409
3410
            // get data, note that "var [x, y] = " structures aren't supported in all JS environments
3411
            var replyData = DiscussionViewer.getReplyData(),
3412
                plainText = replyData[0],
3413
                nickname = replyData[1],
3414
                parentid = DiscussionViewer.getReplyCommentId();
3415
3416
            // do not send if there is no data
3417
            if (plainText.length === 0) {
3418
                // revert loading status…
3419
                Alert.hideLoading();
3420
                Alert.setCustomHandler(null);
3421
                TopNav.showViewButtons();
3422
                return;
3423
            }
3424
3425
            // check entropy
3426
            if (!checkRequirements(function () {
3427
                me.sendComment();
3428
            })) {
3429
                return; // to prevent multiple executions
3430
            }
3431
            Alert.showLoading(null, 10);
3432
3433
            // prepare Uploader
3434
            Uploader.prepare();
3435
            Uploader.setCryptParameters(Prompt.getPassword(), Model.getPasteKey());
3436
3437
            // set success/fail functions
3438
            Uploader.setSuccess(showUploadedComment);
3439
            Uploader.setFailure(function (status, data) {
3440
                // revert loading status…
3441
                Alert.hideLoading();
3442
                TopNav.showViewButtons();
3443
3444
                // show error message
3445
                Alert.showError(Uploader.parseUploadError(status, data, 'post comment'));
3446
3447
                // reset error handler
3448
                Alert.setCustomHandler(null);
3449
            });
3450
3451
            // fill it with unencrypted params
3452
            Uploader.setUnencryptedData('pasteid', Model.getPasteId());
3453
            if (typeof parentid === 'undefined') {
3454
                // if parent id is not set, this is the top-most comment, so use
3455
                // paste id as parent @TODO is this really good?
3456
                Uploader.setUnencryptedData('parentid', Model.getPasteId());
3457
            } else {
3458
                Uploader.setUnencryptedData('parentid', parentid);
3459
            }
3460
3461
            // encrypt data
3462
            Uploader.setData('data', plainText);
3463
3464
            if (nickname.length > 0) {
3465
                Uploader.setData('nickname', nickname);
3466
            }
3467
3468
            Uploader.run();
3469
        }
3470
3471
        /**
3472
         * sends a new paste to server
3473
         *
3474
         * @name   PasteEncrypter.sendPaste
3475
         * @function
3476
         */
3477
        me.sendPaste = function()
3478
        {
3479
            // hide previous (error) messages
3480
            Controller.hideStatusMessages();
3481
3482
            // UI loading state
3483
            TopNav.hideAllButtons();
3484
            Alert.showLoading('Sending paste…', 0, 'cloud-upload');
3485
            TopNav.collapseBar();
3486
3487
            // get data
3488
            var plainText = Editor.getText(),
3489
                format = PasteViewer.getFormat(),
3490
                files = TopNav.getFileList();
3491
3492
            // do not send if there is no data
3493
            if (plainText.length === 0 && files === null) {
3494
                // revert loading status…
3495
                Alert.hideLoading();
3496
                TopNav.showCreateButtons();
3497
                return;
3498
            }
3499
3500
            Alert.showLoading(null, 10);
3501
3502
            // check entropy
3503
            if (!checkRequirements(function () {
3504
                me.sendPaste();
3505
            })) {
3506
                return; // to prevent multiple executions
3507
            }
3508
3509
            // prepare Uploader
3510
            Uploader.prepare();
3511
            Uploader.setCryptParameters(TopNav.getPassword());
3512
3513
            // set success/fail functions
3514
            Uploader.setSuccess(showCreatedPaste);
3515
            Uploader.setFailure(function (status, data) {
3516
                // revert loading status…
3517
                Alert.hideLoading();
3518
                TopNav.showCreateButtons();
3519
3520
                // show error message
3521
                Alert.showError(Uploader.parseUploadError(status, data, 'create paste'));
3522
            });
3523
3524
            // fill it with unencrypted submitted options
3525
            Uploader.setUnencryptedBulkData({
3526
                expire:           TopNav.getExpiration(),
3527
                formatter:        format,
3528
                burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0,
3529
                opendiscussion:   TopNav.getOpenDiscussion() ? 1 : 0
3530
            });
3531
3532
            // prepare PasteViewer for later preview
3533
            PasteViewer.setText(plainText);
3534
            PasteViewer.setFormat(format);
3535
3536
            // encrypt cipher data
3537
            Uploader.setData('data', plainText);
3538
3539
            // encrypt attachments
3540
            encryptAttachments(
3541
                files === null ? null : files[0],
3542
                function () {
3543
                    // send data
3544
                    Uploader.run();
3545
                }
3546
            );
3547
        }
3548
3549
        /**
3550
         * initialize
3551
         *
3552
         * @name   PasteEncrypter.init
3553
         * @function
3554
         */
3555
        me.init = function()
3556
        {
3557
            // nothing yet
3558
        }
3559
3560
        return me;
3561
    })();
3562
3563
    /**
3564
     * (controller) Responsible for decrypting cipherdata and passing data to view.
3565
     *
3566
     * Only decryption, no download.
3567
     *
3568
     * @name PasteDecrypter
3569
     * @class
3570
     */
3571
    var PasteDecrypter = (function () {
3572
        var me = {};
3573
3574
        /**
3575
         * decrypt data or prompts for password in cvase of failure
3576
         *
3577
         * @name   PasteDecrypter.decryptOrPromptPassword
3578
         * @private
3579
         * @function
3580
         * @param  {string} key
3581
         * @param  {string} password - optional, may be an empty string
3582
         * @param  {string} cipherdata
3583
         * @throws {string}
3584
         * @return {false|string} false, when unsuccessful or string (decrypted data)
3585
         */
3586
        function decryptOrPromptPassword(key, password, cipherdata)
3587
        {
3588
            // try decryption without password
3589
            var plaindata = CryptTool.decipher(key, password, cipherdata);
3590
3591
            // if it fails, request password
3592
            if (plaindata.length === 0 && password.length === 0) {
3593
                // try to get cached password first
3594
                password = Prompt.getPassword();
3595
3596
                // if password is there, re-try
3597
                if (password.length === 0) {
3598
                    password = Prompt.requestPassword();
3599
                }
3600
                // recursive
3601
                // note: an infinite loop is prevented as the previous if
3602
                // clause checks whether a password is already set and ignores
3603
                // errors when a password has been passed
3604
                return decryptOrPromptPassword.apply(key, password, cipherdata);
3605
            }
3606
3607
            // if all tries failed, we can only return an error
3608
            if (plaindata.length === 0) {
3609
                throw 'failed to decipher data';
3610
            }
3611
3612
            return plaindata;
3613
        }
3614
3615
        /**
3616
         * decrypt the actual paste text
3617
         *
3618
         * @name   PasteDecrypter.decryptOrPromptPassword
3619
         * @private
3620
         * @function
3621
         * @param  {object} paste - paste data in object form
3622
         * @param  {string} key
3623
         * @param  {string} password
3624
         * @param  {bool} ignoreError - ignore decryption errors iof set to true
3625
         * @return {bool} whether action was successful
3626
         * @throws {string}
3627
         */
3628
        function decryptPaste(paste, key, password, ignoreError)
3629
        {
3630
            var plaintext
3631
            if (ignoreError === true) {
3632
                plaintext = CryptTool.decipher(key, password, paste.data);
3633
            } else {
3634
                try {
3635
                    plaintext = decryptOrPromptPassword(key, password, paste.data);
3636
                } catch (err) {
3637
                    throw 'failed to decipher paste text: ' + err
3638
                }
3639
                if (plaintext === false) {
3640
                    return false;
3641
                }
3642
            }
3643
3644
            // on success show paste
3645
            PasteViewer.setFormat(paste.meta.formatter);
3646
            PasteViewer.setText(plaintext);
3647
            // trigger to show the text (attachment loaded afterwards)
3648
            PasteViewer.run();
3649
3650
            return true;
3651
        }
3652
3653
        /**
3654
         * decrypts any attachment
3655
         *
3656
         * @name   PasteDecrypter.decryptAttachment
3657
         * @private
3658
         * @function
3659
         * @param  {object} paste - paste data in object form
3660
         * @param  {string} key
3661
         * @param  {string} password
3662
         * @return {bool} whether action was successful
3663
         * @throws {string}
3664
         */
3665
        function decryptAttachment(paste, key, password)
3666
        {
3667
            // decrypt attachment
3668
            try {
3669
                var attachment = decryptOrPromptPassword(key, password, paste.attachment);
3670
            } catch (err) {
3671
                throw 'failed to decipher attachment: ' + err
3672
            }
3673
            if (attachment === false) {
3674
                return false;
3675
            }
3676
3677
            // decrypt attachment name
3678
            var attachmentName;
3679
            if (paste.attachmentname) {
3680
                try {
3681
                    attachmentName = decryptOrPromptPassword(key, password, paste.attachmentname);
3682
                } catch (err) {
3683
                    throw 'failed to decipher attachment name: ' + err
3684
                }
3685
                if (attachmentName === false) {
3686
                    return false;
3687
                }
3688
            }
3689
3690
            AttachmentViewer.setAttachment(attachment, attachmentName);
3691
            AttachmentViewer.showAttachment();
3692
3693
            return true;
3694
        }
3695
3696
        /**
3697
         * decrypts all comments and shows them
3698
         *
3699
         * @name   PasteDecrypter.decryptComments
3700
         * @private
3701
         * @function
3702
         * @param  {object} paste - paste data in object form
3703
         * @param  {string} key
3704
         * @param  {string} password
3705
         * @return {bool} whether action was successful
3706
         */
3707
        function decryptComments(paste, key, password)
3708
        {
3709
            // remove potentially previous discussion
3710
            DiscussionViewer.prepareNewDisucssion();
3711
3712
            // iterate over comments
3713
            for (var i = 0; i < paste.comments.length; ++i) {
3714
                var comment = paste.comments[i];
3715
3716
                DiscussionViewer.addComment(
3717
                    comment,
3718
                    CryptTool.decipher(key, password, comment.data),
3719
                    CryptTool.decipher(key, password, comment.meta.nickname)
3720
                );
3721
            }
3722
3723
            DiscussionViewer.finishDiscussion();
3724
            DiscussionViewer.showDiscussion();
3725
            return true;
3726
        }
3727
3728
        /**
3729
         * show decrypted text in the display area, including discussion (if open)
3730
         *
3731
         * @name   PasteDecrypter.run
3732
         * @function
3733
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
3734
         */
3735
        me.run = function(paste)
3736
        {
3737
            Alert.hideMessages();
3738
            Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons
3739
3740
            if (typeof paste === 'undefined') {
3741
                paste = $.parseJSON(Model.getCipherData());
3742
            }
3743
3744
            var key = Model.getPasteKey(),
3745
                password = Prompt.getPassword();
3746
3747
            if (PasteViewer.isPrettyPrinted()) {
3748
                console.error('Too pretty! (don\'t know why this check)'); //@TODO
3749
                return;
3750
            }
3751
3752
            // try to decrypt the paste
3753
            try {
3754
                // decrypt attachments
3755
                if (paste.attachment) {
3756
                    // try to decrypt paste and if it fails (because the password is
3757
                    // missing) return to let JS continue and wait for user
3758
                    if (!decryptAttachment(paste, key, password)) {
3759
                        return;
3760
                    }
3761
                    // ignore empty paste, as this is allowed when pasting attachments
3762
                    decryptPaste(paste, key, password, true);
3763
                } else {
3764
                    decryptPaste(paste, key, password);
3765
                }
3766
3767
3768
                // shows the remaining time (until) deletion
3769
                PasteStatus.showRemainingTime(paste.meta);
3770
3771
                // if the discussion is opened on this paste, display it
3772
                if (paste.meta.opendiscussion) {
3773
                    decryptComments(paste, key, password);
3774
                }
3775
3776
                Alert.hideLoading();
3777
                TopNav.showViewButtons();
3778
            } catch(err) {
3779
                Alert.hideLoading();
3780
3781
                // log and show error
3782
                console.error(err);
3783
                Alert.showError('Could not decrypt data (Wrong key?)');
3784
            }
3785
        }
3786
3787
        /**
3788
         * initialize
3789
         *
3790
         * @name   PasteDecrypter.init
3791
         * @function
3792
         */
3793
        me.init = function()
3794
        {
3795
            // nothing yet
3796
        }
3797
3798
        return me;
3799
    })();
3800
3801
    /**
3802
     * (controller) main PrivateBin logic
3803
     *
3804
     * @name   Controller
3805
     * @param  {object} window
3806
     * @param  {object} document
3807
     * @class
3808
     */
3809
    var Controller = (function (window, document) {
3810
        var me = {};
3811
3812
        /**
3813
         * hides all status messages no matter which module showed them
3814
         *
3815
         * @name   Controller.hideStatusMessages
3816
         * @function
3817
         */
3818
        me.hideStatusMessages = function()
3819
        {
3820
            PasteStatus.hideMessages();
3821
            Alert.hideMessages();
3822
        }
3823
3824
        /**
3825
         * creates a new paste
3826
         *
3827
         * @name   Controller.newPaste
3828
         * @function
3829
         */
3830
        me.newPaste = function()
3831
        {
3832
            // Important: This *must not* run Alert.hideMessages() as previous
3833
            // errors from viewing a paste should be shown.
3834
            TopNav.hideAllButtons();
3835
            Alert.showLoading('Preparing new paste…', 0, 'time');
3836
3837
            PasteStatus.hideMessages();
3838
            PasteViewer.hide();
3839
            Editor.resetInput();
3840
            Editor.show();
3841
            Editor.focusInput();
3842
3843
            TopNav.showCreateButtons();
3844
            Alert.hideLoading();
3845
        }
3846
3847
        /**
3848
         * shows the loaded paste
3849
         *
3850
         * @name   Controller.showPaste
3851
         * @function
3852
         */
3853
        me.showPaste = function()
3854
        {
3855
            try {
3856
                Model.getPasteId();
3857
                Model.getPasteKey();
3858
            } catch (err) {
3859
                console.error(err);
3860
3861
                // missing decryption key (or paste ID) in URL?
3862
                if (window.location.hash.length === 0) {
3863
                    Alert.showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)');
3864
                    // @TODO adjust error message as it is less specific now, probably include thrown exception for a detailed error
3865
                    return;
3866
                }
3867
            }
3868
3869
            // show proper elements on screen
3870
            PasteDecrypter.run();
3871
        }
3872
3873
        /**
3874
         * refreshes the loaded paste to show potential new data
3875
         *
3876
         * @name   Controller.refreshPaste
3877
         * @function
3878
         * @param  {function} callback
3879
         */
3880
        me.refreshPaste = function(callback)
3881
        {
3882
            // save window position to restore it later
3883
            var orgPosition = $(window).scrollTop();
3884
3885
            Uploader.prepare();
3886
            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
3887
3888
            Uploader.setFailure(function (status, data) {
3889
                // revert loading status…
3890
                Alert.hideLoading();
3891
                TopNav.showViewButtons();
3892
3893
                // show error message
3894
                Alert.showError(Uploader.parseUploadError(status, data, 'refresh display'));
3895
            })
3896
            Uploader.setSuccess(function (status, data) {
3897
                PasteDecrypter.run(data);
3898
3899
                // restore position
3900
                window.scrollTo(0, orgPosition);
3901
3902
                callback();
3903
            })
3904
            Uploader.run();
3905
        }
3906
3907
        /**
3908
         * clone the current paste
3909
         *
3910
         * @name   Controller.clonePaste
3911
         * @function
3912
         * @param  {Event} event
3913
         */
3914
        me.clonePaste = function(event)
3915
        {
3916
            TopNav.collapseBar();
3917
            TopNav.hideAllButtons();
3918
            Alert.showLoading('Cloning paste…', 0, 'transfer');
3919
3920
            // hide messages from previous paste
3921
            me.hideStatusMessages();
3922
3923
            // erase the id and the key in url
3924
            history.pushState({type: 'clone'}, document.title, Helper.baseUri());
3925
3926
            if (AttachmentViewer.hasAttachment()) {
3927
                AttachmentViewer.moveAttachmentTo(
3928
                    TopNav.getCustomAttachment(),
3929
                    'Cloned: \'%s\''
3930
                );
3931
                TopNav.hideFileSelector();
3932
                AttachmentViewer.hideAttachment();
3933
                // NOTE: it also looks nice without removing the attachment
3934
                // but for a consistent display we remove it…
3935
                AttachmentViewer.hideAttachmentPreview();
3936
                TopNav.showCustomAttachment();
3937
3938
                // show another status message to make the user aware that the
3939
                // file was cloned too!
3940
                Alert.showStatus(
3941
                    [
3942
                        'The cloned file \'%s\' was attached to this paste.',
3943
                        AttachmentViewer.getAttachment()[1]
3944
                    ], 'copy', true, true);
3945
            }
3946
3947
            Editor.setText(PasteViewer.getText())
3948
            PasteViewer.hide();
3949
            Editor.show();
3950
3951
            Alert.hideLoading();
3952
            TopNav.showCreateButtons();
3953
        }
3954
3955
        /**
3956
         * removes a saved paste
3957
         *
3958
         * @name   Controller.removePaste
3959
         * @function
3960
         * @param  {string} pasteId
3961
         * @param  {string} deleteToken
3962
         */
3963
        me.removePaste = function(pasteId, deleteToken) {
3964
            // unfortunately many web servers don't support DELETE (and PUT) out of the box
3965
            // so we use a POST request
3966
            Uploader.prepare();
3967
            Uploader.setUrl(Helper.baseUri() + '?' + pasteId);
3968
            Uploader.setUnencryptedData('deletetoken', deleteToken);
3969
3970
            Uploader.setFailure(function () {
3971
                Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
3972
            })
3973
            Uploader.run();
3974
        }
3975
3976
        /**
3977
         * application start
3978
         *
3979
         * @name   Controller.init
3980
         * @function
3981
         */
3982
        me.init = function()
3983
        {
3984
            // first load translations
3985
            I18n.loadTranslations();
3986
3987
            // initialize other modules/"classes"
3988
            Alert.init();
3989
            Model.init();
3990
3991
            AttachmentViewer.init();
3992
            DiscussionViewer.init();
3993
            Editor.init();
3994
            PasteDecrypter.init();
3995
            PasteEncrypter.init();
3996
            PasteStatus.init();
3997
            PasteViewer.init();
3998
            Prompt.init();
3999
            TopNav.init();
4000
            UiHelper.init();
4001
            Uploader.init();
4002
4003
            // display an existing paste
4004
            if (Model.hasCipherData()) {
4005
                return me.showPaste();
4006
            }
4007
4008
            // otherwise create a new paste
4009
            me.newPaste();
4010
        }
4011
4012
        return me;
4013
    })(window, document);
4014
4015
    return {
4016
        Helper: Helper,
4017
        I18n: I18n,
4018
        CryptTool: CryptTool,
4019
        Model: Model,
4020
        UiHelper: UiHelper,
4021
        Alert: Alert,
4022
        PasteStatus: PasteStatus,
4023
        Prompt: Prompt,
4024
        Editor: Editor,
4025
        PasteViewer: PasteViewer,
4026
        AttachmentViewer: AttachmentViewer,
4027
        DiscussionViewer: DiscussionViewer,
4028
        TopNav: TopNav,
4029
        Uploader: Uploader,
4030
        PasteEncrypter: PasteEncrypter,
4031
        PasteDecrypter: PasteDecrypter,
4032
        Controller: Controller
4033
    };
4034
}(jQuery, sjcl, Base64, RawDeflate);
4035