Completed
Push — master ( afdfcb...27c4d6 )
by El
01:29
created

controller.historyChange   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 11
rs 9.2
1
/**
2
 * PrivateBin
3
 *
4
 * a zero-knowledge paste bin
5
 *
6
 * @see       {@link https://github.com/PrivateBin/PrivateBin}
7
 * @copyright 2012 Sébastien SAUVAGE ({@link http://sebsauvage.net})
8
 * @license   {@link https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
9
 * @version   1.1
10
 * @name      PrivateBin
11
 * @namespace
12
 */
13
14
'use strict';
15
/** global: Base64 */
16
/** global: FileReader */
17
/** global: RawDeflate */
18
/** global: history */
19
/** global: navigator */
20
/** global: prettyPrint */
21
/** global: prettyPrintOne */
22
/** global: showdown */
23
/** global: sjcl */
24
25
// Immediately start random number generator collector.
26
sjcl.random.startCollectors();
27
28
jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
29
    /**
30
     * static helper methods
31
     *
32
     * @name helper
33
     * @class
34
     */
35
    var helper = {
36
        /**
37
         * converts a duration (in seconds) into human friendly approximation
38
         *
39
         * @name helper.secondsToHuman
40
         * @function
41
         * @param  {number} seconds
42
         * @return {Array}
43
         */
44
        secondsToHuman: function(seconds)
45
        {
46
            var v;
47
            if (seconds < 60)
48
            {
49
                v = Math.floor(seconds);
50
                return [v, 'second'];
51
            }
52
            if (seconds < 60 * 60)
53
            {
54
                v = Math.floor(seconds / 60);
55
                return [v, 'minute'];
56
            }
57
            if (seconds < 60 * 60 * 24)
58
            {
59
                v = Math.floor(seconds / (60 * 60));
60
                return [v, 'hour'];
61
            }
62
            // If less than 2 months, display in days:
63
            if (seconds < 60 * 60 * 24 * 60)
64
            {
65
                v = Math.floor(seconds / (60 * 60 * 24));
66
                return [v, 'day'];
67
            }
68
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
69
            return [v, 'month'];
70
        },
71
72
        /**
73
         * text range selection
74
         *
75
         * @see    {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
76
         * @name   helper.selectText
77
         * @function
78
         * @param  {string} element - Indentifier of the element to select (id="")
79
         */
80
        selectText: function(element)
81
        {
82
            var doc = document,
83
                text = doc.getElementById(element),
84
                range,
85
                selection;
86
87
            // MS
88
            if (doc.body.createTextRange)
89
            {
90
                range = doc.body.createTextRange();
91
                range.moveToElementText(text);
92
                range.select();
93
            }
94
            // all others
95
            else if (window.getSelection)
96
            {
97
                selection = window.getSelection();
98
                range = doc.createRange();
99
                range.selectNodeContents(text);
100
                selection.removeAllRanges();
101
                selection.addRange(range);
102
            }
103
        },
104
105
        /**
106
         * set text of a DOM element (required for IE),
107
         * this is equivalent to element.text(text)
108
         *
109
         * @name   helper.setElementText
110
         * @function
111
         * @param  {Object} element - a DOM element
112
         * @param  {string} text - the text to enter
113
         */
114
        setElementText: function(element, text)
115
        {
116
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
117
            if ($('#oldienotice').is(':visible')) {
118
                var html = this.htmlEntities(text).replace(/\n/ig, '\r\n<br>');
119
                element.html('<pre>' + html + '</pre>');
120
            }
121
            // for other (sane) browsers:
122
            else
123
            {
124
                element.text(text);
125
            }
126
        },
127
128
        /**
129
         * replace last child of element with message
130
         *
131
         * @name   helper.setMessage
132
         * @function
133
         * @param  {Object} element - a jQuery wrapped DOM element
134
         * @param  {string} message - the message to append
135
         */
136
        setMessage: function(element, message)
137
        {
138
            var content = element.contents();
139
            if (content.length > 0)
140
            {
141
                content[content.length - 1].nodeValue = ' ' + message;
142
            }
143
            else
144
            {
145
                this.setElementText(element, message);
146
            }
147
        },
148
149
        /**
150
         * convert URLs to clickable links.
151
         * URLs to handle:
152
         * <pre>
153
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
154
         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
155
         *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
156
         * </pre>
157
         *
158
         * @name   helper.urls2links
159
         * @function
160
         * @param  {Object} element - a jQuery DOM element
161
         */
162
        urls2links: function(element)
163
        {
164
            var markup = '<a href="$1" rel="nofollow">$1</a>';
165
            element.html(
166
                element.html().replace(
167
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
168
                    markup
169
                )
170
            );
171
            element.html(
172
                element.html().replace(
173
                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
174
                    markup
175
                )
176
            );
177
        },
178
179
        /**
180
         * minimal sprintf emulation for %s and %d formats
181
         *
182
         * @see    {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
183
         * @name   helper.sprintf
184
         * @function
185
         * @param  {string} format
0 ignored issues
show
Documentation introduced by
The parameter format does not exist. Did you maybe forget to remove this comment?
Loading history...
186
         * @param  {...*} args - one or multiple parameters injected into format string
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
187
         * @return {string}
188
         */
189
        sprintf: function()
190
        {
191
            var args = arguments;
192
            if (typeof arguments[0] === 'object')
193
            {
194
                args = arguments[0];
195
            }
196
            var format = args[0],
197
                i = 1;
198
            return format.replace(/%((%)|s|d)/g, function (m) {
199
                // m is the matched format, e.g. %s, %d
200
                var val;
201
                if (m[2]) {
202
                    val = m[2];
203
                } else {
204
                    val = args[i];
205
                    // A switch statement so that the formatter can be extended.
206
                    switch (m)
207
                    {
208
                        case '%d':
209
                            val = parseFloat(val);
210
                            if (isNaN(val)) {
211
                                val = 0;
212
                            }
213
                            break;
214
                        default:
215
                            // Default is %s
216
                    }
217
                    ++i;
218
                }
219
                return val;
220
            });
221
        },
222
223
        /**
224
         * get value of cookie, if it was set, empty string otherwise
225
         *
226
         * @see    {@link http://www.w3schools.com/js/js_cookies.asp}
227
         * @name   helper.getCookie
228
         * @function
229
         * @param  {string} cname
230
         * @return {string}
231
         */
232
        getCookie: function(cname) {
233
            var name = cname + '=',
234
                ca = document.cookie.split(';');
235
            for (var i = 0; i < ca.length; ++i) {
236
                var c = ca[i];
237
                while (c.charAt(0) === ' ')
238
                {
239
                    c = c.substring(1);
240
                }
241
                if (c.indexOf(name) === 0)
242
                {
243
                    return c.substring(name.length, c.length);
244
                }
245
            }
246
            return '';
247
        },
248
249
        /**
250
         * get the current script location (without search or hash part of the URL),
251
         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
252
         *
253
         * @name   helper.scriptLocation
254
         * @function
255
         * @return {string} current script location
256
         */
257
        scriptLocation: function()
258
        {
259
            var scriptLocation = window.location.href.substring(
260
                    0,
261
                    window.location.href.length - window.location.search.length - window.location.hash.length
262
                ),
263
                hashIndex = scriptLocation.indexOf('?');
264
            if (hashIndex !== -1)
265
            {
266
                scriptLocation = scriptLocation.substring(0, hashIndex);
267
            }
268
            return scriptLocation;
269
        },
270
271
        /**
272
         * get the pastes unique identifier from the URL,
273
         * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
274
         *
275
         * @name   helper.pasteId
276
         * @function
277
         * @return {string} unique identifier
278
         */
279
        pasteId: function()
280
        {
281
            return window.location.search.substring(1);
282
        },
283
284
        /**
285
         * return the deciphering key stored in anchor part of the URL
286
         *
287
         * @name   helper.pageKey
288
         * @function
289
         * @return {string} key
290
         */
291
        pageKey: function()
292
        {
293
            var key = window.location.hash.substring(1),
294
                i = key.indexOf('&');
295
296
            // Some web 2.0 services and redirectors add data AFTER the anchor
297
            // (such as &utm_source=...). We will strip any additional data.
298
            if (i > -1)
299
            {
300
                key = key.substring(0, i);
301
            }
302
303
            return key;
304
        },
305
306
        /**
307
         * convert all applicable characters to HTML entities
308
         *
309
         * @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}
310
         * @name   helper.htmlEntities
311
         * @function
312
         * @param  {string} str
313
         * @return {string} escaped HTML
314
         */
315
        htmlEntities: function(str) {
316
            return String(str).replace(
317
                /[&<>"'`=\/]/g, function(s) {
318
                    return helper.entityMap[s];
319
                });
320
        },
321
322
        /**
323
         * character to HTML entity lookup table
324
         *
325
         * @see    {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
326
         * @name   helper.entityMap
327
         * @enum   {Object}
328
         * @readonly
329
         */
330
        entityMap: {
331
            '&': '&amp;',
332
            '<': '&lt;',
333
            '>': '&gt;',
334
            '"': '&quot;',
335
            "'": '&#39;',
336
            '/': '&#x2F;',
337
            '`': '&#x60;',
338
            '=': '&#x3D;'
339
        }
340
    };
341
342
    /**
343
     * internationalization methods
344
     *
345
     * @name i18n
346
     * @class
347
     */
348
    var i18n = {
349
        /**
350
         * supported languages, minus the built in 'en'
351
         *
352
         * @name   i18n.supportedLanguages
353
         * @prop   {string[]}
354
         * @readonly
355
         */
356
        supportedLanguages: ['de', 'es', 'fr', 'it', 'no', 'pl', 'oc', 'ru', 'sl', 'zh'],
357
358
        /**
359
         * translate a string, alias for i18n.translate()
360
         *
361
         * @name   i18n._
362
         * @function
363
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
364
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
365
         * @return {string}
366
         */
367
        _: function()
368
        {
369
            return this.translate(arguments);
370
        },
371
372
        /**
373
         * translate a string
374
         *
375
         * @name   i18n.translate
376
         * @function
377
         * @param  {string} messageId
0 ignored issues
show
Documentation introduced by
The parameter messageId does not exist. Did you maybe forget to remove this comment?
Loading history...
378
         * @param  {...*} args - one or multiple parameters injected into placeholders
0 ignored issues
show
Documentation introduced by
The parameter args does not exist. Did you maybe forget to remove this comment?
Loading history...
379
         * @return {string}
380
         */
381
        translate: function()
382
        {
383
            var args = arguments, messageId;
384
            if (typeof arguments[0] === 'object')
385
            {
386
                args = arguments[0];
387
            }
388
            var usesPlurals = $.isArray(args[0]);
389
            if (usesPlurals)
390
            {
391
                // use the first plural form as messageId, otherwise the singular
392
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
393
            }
394
            else
395
            {
396
                messageId = args[0];
397
            }
398
            if (messageId.length === 0)
399
            {
400
                return messageId;
401
            }
402
            if (!this.translations.hasOwnProperty(messageId))
403
            {
404
                if (this.language !== 'en')
405
                {
406
                    console.debug(
407
                        'Missing ' + this.language + ' translation for: ' + messageId
408
                    );
409
                }
410
                this.translations[messageId] = args[0];
411
            }
412
            if (usesPlurals && $.isArray(this.translations[messageId]))
413
            {
414
                var n = parseInt(args[1] || 1, 10),
415
                    key = this.getPluralForm(n),
416
                    maxKey = this.translations[messageId].length - 1;
417
                if (key > maxKey)
418
                {
419
                    key = maxKey;
420
                }
421
                args[0] = this.translations[messageId][key];
422
                args[1] = n;
423
            }
424
            else
425
            {
426
                args[0] = this.translations[messageId];
427
            }
428
            return helper.sprintf(args);
429
        },
430
431
        /**
432
         * per language functions to use to determine the plural form
433
         *
434
         * @see    {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
435
         * @name   i18n.getPluralForm
436
         * @function
437
         * @param  {number} n
438
         * @return {number} array key
439
         */
440
        getPluralForm: function(n) {
441
            switch (this.language)
442
            {
443
                case 'fr':
444
                case 'oc':
445
                case 'zh':
446
                    return (n > 1 ? 1 : 0);
447
                case 'pl':
448
                    return (n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
449
                case 'ru':
450
                    return (n % 10 === 1 && n % 100 !== 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2));
451
                case 'sl':
452
                    return (n % 100 === 1 ? 1 : (n % 100 === 2 ? 2 : (n % 100 === 3 || n % 100 === 4 ? 3 : 0)));
453
                // de, en, es, it, no
454
                default:
455
                    return (n !== 1 ? 1 : 0);
456
            }
457
        },
458
459
        /**
460
         * load translations into cache, then trigger controller initialization
461
         *
462
         * @name   i18n.loadTranslations
463
         * @function
464
         */
465
        loadTranslations: function()
466
        {
467
            var language = helper.getCookie('lang');
468
            if (language.length === 0)
469
            {
470
                language = (navigator.language || navigator.userLanguage).substring(0, 2);
471
            }
472
            // note that 'en' is built in, so no translation is necessary
473
            if (i18n.supportedLanguages.indexOf(language) === -1)
474
            {
475
                controller.init();
476
            }
477
            else
478
            {
479
                $.getJSON('i18n/' + language + '.json', function(data) {
480
                    i18n.language = language;
481
                    i18n.translations = data;
482
                    controller.init();
483
                });
484
            }
485
        },
486
487
        /**
488
         * built in language
489
         *
490
         * @name   i18n.language
491
         * @prop   {string}
492
         */
493
        language: 'en',
494
495
        /**
496
         * translation cache
497
         *
498
         * @name   i18n.translations
499
         * @enum   {Object}
500
         */
501
        translations: {}
502
    };
503
504
    /**
505
     * filter methods
506
     *
507
     * @name filter
508
     * @class
509
     */
510
    var filter = {
511
        /**
512
         * compress a message (deflate compression), returns base64 encoded data
513
         *
514
         * @name   filter.compress
515
         * @function
516
         * @param  {string} message
517
         * @return {string} base64 data
518
         */
519
        compress: function(message)
520
        {
521
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
522
        },
523
524
        /**
525
         * decompress a message compressed with filter.compress()
526
         *
527
         * @name   filter.decompress
528
         * @function
529
         * @param  {string} data - base64 data
530
         * @return {string} message
531
         */
532
        decompress: function(data)
533
        {
534
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
535
        },
536
537
        /**
538
         * compress, then encrypt message with given key and password
539
         *
540
         * @name   filter.cipher
541
         * @function
542
         * @param  {string} key
543
         * @param  {string} password
544
         * @param  {string} message
545
         * @return {string} data - JSON with encrypted data
546
         */
547
        cipher: function(key, password, message)
548
        {
549
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
550
            var options = {mode: 'gcm', ks: 256, ts: 128};
551
            if ((password || '').trim().length === 0)
552
            {
553
                return sjcl.encrypt(key, this.compress(message), options);
554
            }
555
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.compress(message), options);
556
        },
557
558
        /**
559
         * decrypt message with key, then decompress
560
         *
561
         * @name   filter.decipher
562
         * @function
563
         * @param  {string} key
564
         * @param  {string} password
565
         * @param  {string} data - JSON with encrypted data
566
         * @return {string} decrypted message
567
         */
568
        decipher: function(key, password, data)
569
        {
570
            if (data !== undefined)
571
            {
572
                try
573
                {
574
                    return this.decompress(sjcl.decrypt(key, data));
575
                }
576
                catch(err)
577
                {
578
                    try
579
                    {
580
                        return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
581
                    }
582
                    catch(e)
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
583
                    {}
584
                }
585
            }
586
            return '';
587
        }
588
    };
589
590
    /**
591
     * PrivateBin logic
592
     *
593
     * @name controller
594
     * @class
595
     */
596
    var controller = {
597
        /**
598
         * headers to send in AJAX requests
599
         *
600
         * @name   controller.headers
601
         * @enum   {Object}
602
         */
603
        headers: {'X-Requested-With': 'JSONHttpRequest'},
604
605
        /**
606
         * URL shortners create address
607
         *
608
         * @name   controller.shortenerUrl
609
         * @prop   {string}
610
         */
611
        shortenerUrl: '',
612
613
        /**
614
         * URL of newly created paste
615
         *
616
         * @name   controller.createdPasteUrl
617
         * @prop   {string}
618
         */
619
        createdPasteUrl: '',
620
621
        /**
622
         * ask the user for the password and set it
623
         *
624
         * @name   controller.requestPassword
625
         * @function
626
         */
627
        requestPassword: function()
628
        {
629
            if (this.passwordModal.length === 0) {
630
                var password = prompt(i18n._('Please enter the password for this paste:'), '');
631
                if (password === null)
632
                {
633
                    throw 'password prompt canceled';
634
                }
635
                if (password.length === 0)
636
                {
637
                    this.requestPassword();
638
                } else {
639
                    this.passwordInput.val(password);
640
                    this.displayMessages();
641
                }
642
            } else {
643
                this.passwordModal.modal();
644
            }
645
        },
646
647
        /**
648
         * use given format on paste, defaults to plain text
649
         *
650
         * @name   controller.formatPaste
651
         * @function
652
         * @param  {string} format
653
         * @param  {string} text
654
         */
655
        formatPaste: function(format, text)
656
        {
657
            helper.setElementText(this.clearText, text);
658
            helper.setElementText(this.prettyPrint, text);
659
            switch (format || 'plaintext')
660
            {
661
                case 'markdown':
662
                    if (typeof showdown === 'object')
663
                    {
664
                        var converter = new showdown.Converter({
665
                            strikethrough: true,
666
                            tables: true,
667
                            tablesHeaderId: true
668
                        });
669
                        this.clearText.html(
670
                            converter.makeHtml(text)
671
                        );
672
                        // add table classes from bootstrap css
673
                        this.clearText.find('table').addClass('table-condensed table-bordered');
674
675
                        this.clearText.removeClass('hidden');
676
                    }
677
                    this.prettyMessage.addClass('hidden');
678
                    break;
679
                case 'syntaxhighlighting':
680
                    if (typeof prettyPrintOne === 'function')
681
                    {
682
                        if (typeof prettyPrint === 'function')
683
                        {
684
                            prettyPrint();
685
                        }
686
                        this.prettyPrint.html(
687
                            prettyPrintOne(
688
                                helper.htmlEntities(text), null, true
689
                            )
690
                        );
691
                    }
692
                    // fall through, as the rest is the same
693
                default:
694
                    // convert URLs to clickable links
695
                    helper.urls2links(this.clearText);
696
                    helper.urls2links(this.prettyPrint);
697
                    this.clearText.addClass('hidden');
698
                    if (format === 'plaintext')
699
                    {
700
                        this.prettyPrint.css('white-space', 'pre-wrap');
701
                        this.prettyPrint.css('word-break', 'normal');
702
                        this.prettyPrint.removeClass('prettyprint');
703
                    }
704
                    this.prettyMessage.removeClass('hidden');
705
            }
706
        },
707
708
        /**
709
         * show decrypted text in the display area, including discussion (if open)
710
         *
711
         * @name   controller.displayMessages
712
         * @function
713
         * @param  {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
0 ignored issues
show
Documentation Bug introduced by
The parameter [paste] does not exist. Did you maybe mean paste instead?
Loading history...
714
         */
715
        displayMessages: function(paste)
716
        {
717
            paste = paste || $.parseJSON(this.cipherData.text());
718
            var key = helper.pageKey(),
719
                password = this.passwordInput.val();
720
            if (!this.prettyPrint.hasClass('prettyprinted')) {
721
                // Try to decrypt the paste.
722
                try
723
                {
724
                    if (paste.attachment)
725
                    {
726
                        var attachment = filter.decipher(key, password, paste.attachment);
727
                        if (attachment.length === 0)
728
                        {
729
                            if (password.length === 0)
730
                            {
731
                                this.requestPassword();
732
                                return;
733
                            }
734
                            attachment = filter.decipher(key, password, paste.attachment);
735
                        }
736
                        if (attachment.length === 0)
737
                        {
738
                            throw 'failed to decipher attachment';
739
                        }
740
741
                        if (paste.attachmentname)
742
                        {
743
                            var attachmentname = filter.decipher(key, password, paste.attachmentname);
744
                            if (attachmentname.length > 0)
745
                            {
746
                                this.attachmentLink.attr('download', attachmentname);
747
                            }
748
                        }
749
                        this.attachmentLink.attr('href', attachment);
750
                        this.attachment.removeClass('hidden');
751
752
                        // if the attachment is an image, display it
753
                        var imagePrefix = 'data:image/';
754
                        if (attachment.substring(0, imagePrefix.length) === imagePrefix)
755
                        {
756
                            this.image.html(
757
                                $(document.createElement('img'))
758
                                    .attr('src', attachment)
759
                                    .attr('class', 'img-thumbnail')
760
                            );
761
                            this.image.removeClass('hidden');
762
                        }
763
                    }
764
                    var cleartext = filter.decipher(key, password, paste.data);
765
                    if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
766
                    {
767
                        this.requestPassword();
768
                        return;
769
                    }
770
                    if (cleartext.length === 0 && !paste.attachment)
771
                    {
772
                        throw 'failed to decipher message';
773
                    }
774
775
                    this.passwordInput.val(password);
776
                    if (cleartext.length > 0)
777
                    {
778
                        $('#pasteFormatter').val(paste.meta.formatter);
779
                        this.formatPaste(paste.meta.formatter, cleartext);
780
                    }
781
                }
782
                catch(err)
783
                {
784
                    this.clearText.addClass('hidden');
785
                    this.prettyMessage.addClass('hidden');
786
                    this.cloneButton.addClass('hidden');
787
                    this.showError(i18n._('Could not decrypt data (Wrong key?)'));
788
                    return;
789
                }
790
            }
791
792
            // display paste expiration / for your eyes only
793
            if (paste.meta.expire_date)
794
            {
795
                var expiration = helper.secondsToHuman(paste.meta.remaining_time),
796
                    expirationLabel = [
797
                        'This document will expire in %d ' + expiration[1] + '.',
798
                        'This document will expire in %d ' + expiration[1] + 's.'
799
                    ];
800
                helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0]));
801
                this.remainingTime.removeClass('foryoureyesonly')
802
                                  .removeClass('hidden');
803
            }
804
            if (paste.meta.burnafterreading)
805
            {
806
                // unfortunately many web servers don't support DELETE (and PUT) out of the box
807
                $.ajax({
808
                    type: 'POST',
809
                    url: helper.scriptLocation() + '?' + helper.pasteId(),
810
                    data: {deletetoken: 'burnafterreading'},
811
                    dataType: 'json',
812
                    headers: this.headers
813
                })
814
                .fail(function() {
815
                    controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
816
                });
817
                helper.setMessage(this.remainingTime, i18n._(
818
                    'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
819
                ));
820
                this.remainingTime.addClass('foryoureyesonly')
821
                                  .removeClass('hidden');
822
                // discourage cloning (as it can't really be prevented)
823
                this.cloneButton.addClass('hidden');
824
            }
825
826
            // if the discussion is opened on this paste, display it
827
            if (paste.meta.opendiscussion)
828
            {
829
                this.comments.html('');
830
831
                // iterate over comments
832
                for (var i = 0; i < paste.comments.length; ++i)
833
                {
834
                    var place = this.comments,
835
                        comment = paste.comments[i],
836
                        commenttext = filter.decipher(key, password, comment.data),
837
                        // if parent comment exists, display below (CSS will automatically shift it to the right)
838
                        cname = '#comment_' + comment.parentid,
839
                        divComment = $('<article><div class="comment" id="comment_' + comment.id
840
                                   + '"><div class="commentmeta"><span class="nickname"></span>'
841
                                   + '<span class="commentdate"></span></div>'
842
                                   + '<div class="commentdata"></div>'
843
                                   + '<button class="btn btn-default btn-sm">'
844
                                   + i18n._('Reply') + '</button></div></article>'),
845
                        divCommentData = divComment.find('div.commentdata');
846
847
                    // if the element exists in page
848
                    if ($(cname).length)
849
                    {
850
                        place = $(cname);
851
                    }
852
                    divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this));
853
                    helper.setElementText(divCommentData, commenttext);
854
                    helper.urls2links(divCommentData);
855
856
                    // try to get optional nickname
857
                    var nick = filter.decipher(key, password, comment.meta.nickname);
858
                    if (nick.length > 0)
859
                    {
860
                        divComment.find('span.nickname').text(nick);
861
                    }
862
                    else
863
                    {
864
                        divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
865
                    }
866
                    divComment.find('span.commentdate')
867
                              .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
868
                              .attr('title', 'CommentID: ' + comment.id);
869
870
                    // if an avatar is available, display it
871
                    if (comment.meta.vizhash)
872
                    {
873
                        divComment.find('span.nickname')
874
                                  .before(
875
                                    '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
876
                                    i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
877
                                  );
878
                    }
879
880
                    place.append(divComment);
881
                }
882
                var divComment = $(
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable divComment already seems to be declared on line 839. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
883
                    '<div class="comment"><button class="btn btn-default btn-sm">' +
884
                    i18n._('Add comment') + '</button></div>'
885
                );
886
                divComment.find('button').click({commentid: helper.pasteId()}, $.proxy(this.openReply, this));
887
                this.comments.append(divComment);
888
                this.discussion.removeClass('hidden');
889
            }
890
        },
891
892
        /**
893
         * open the comment entry when clicking the "Reply" button of a comment
894
         *
895
         * @name   controller.openReply
896
         * @function
897
         * @param  {Event} event
898
         */
899
        openReply: function(event)
900
        {
901
            event.preventDefault();
902
903
            // remove any other reply area
904
            $('div.reply').remove();
905
906
            var source = $(event.target),
907
                commentid = event.data.commentid,
908
                hint = i18n._('Optional nickname...'),
909
                reply = $(
910
                    '<div class="reply"><input type="text" id="nickname" ' +
911
                    'class="form-control" title="' + hint + '" placeholder="' +
912
                    hint + '" /><textarea id="replymessage" class="replymessage ' +
913
                    'form-control" cols="80" rows="7"></textarea><br />' +
914
                    '<div id="replystatus"></div><button id="replybutton" ' +
915
                    'class="btn btn-default btn-sm">' + i18n._('Post comment') +
916
                    '</button></div>'
917
                );
918
            reply.find('button').click(
919
                {parentid: commentid},
920
                $.proxy(this.sendComment, this)
921
            );
922
            source.after(reply);
923
            this.replyStatus = $('#replystatus');
924
            $('#replymessage').focus();
925
        },
926
927
        /**
928
         * send a reply in a discussion
929
         *
930
         * @name   controller.sendComment
931
         * @function
932
         * @param  {Event} event
933
         */
934
        sendComment: function(event)
935
        {
936
            event.preventDefault();
937
            this.errorMessage.addClass('hidden');
938
            // do not send if no data
939
            var replyMessage = $('#replymessage');
940
            if (replyMessage.val().length === 0)
941
            {
942
                return;
943
            }
944
945
            this.showStatus(i18n._('Sending comment...'), true);
946
            var parentid = event.data.parentid,
947
                key = helper.pageKey(),
948
                cipherdata = filter.cipher(key, this.passwordInput.val(), replyMessage.val()),
949
                ciphernickname = '',
950
                nick = $('#nickname').val();
951
            if (nick.length > 0)
952
            {
953
                ciphernickname = filter.cipher(key, this.passwordInput.val(), nick);
954
            }
955
            var data_to_send = {
956
                data:     cipherdata,
957
                parentid: parentid,
958
                pasteid:  helper.pasteId(),
959
                nickname: ciphernickname
960
            };
961
962
            $.ajax({
963
                type: 'POST',
964
                url: helper.scriptLocation(),
965
                data: data_to_send,
966
                dataType: 'json',
967
                headers: this.headers,
968
                success: function(data)
969
                {
970
                    if (data.status === 0)
971
                    {
972
                        controller.showStatus(i18n._('Comment posted.'));
973
                        $.ajax({
974
                            type: 'GET',
975
                            url: helper.scriptLocation() + '?' + helper.pasteId(),
976
                            dataType: 'json',
977
                            headers: controller.headers,
978
                            success: function(data)
979
                            {
980
                                if (data.status === 0)
981
                                {
982
                                    controller.displayMessages(data);
983
                                }
984
                                else if (data.status === 1)
985
                                {
986
                                    controller.showError(i18n._('Could not refresh display: %s', data.message));
987
                                }
988
                                else
989
                                {
990
                                    controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
991
                                }
992
                            }
993
                        })
994
                        .fail(function() {
995
                            controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
996
                        });
997
                    }
998
                    else if (data.status === 1)
999
                    {
1000
                        controller.showError(i18n._('Could not post comment: %s', data.message));
1001
                    }
1002
                    else
1003
                    {
1004
                        controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
1005
                    }
1006
                }
1007
            })
1008
            .fail(function() {
1009
                controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
1010
            });
1011
        },
1012
1013
        /**
1014
         * send a new paste to server
1015
         *
1016
         * @name   controller.sendData
1017
         * @function
1018
         * @param  {Event} event
1019
         */
1020
        sendData: function(event)
1021
        {
1022
            event.preventDefault();
1023
            var file = document.getElementById('file'),
1024
                files = (file && file.files) ? file.files : null; // FileList object
1025
1026
            // do not send if no data.
1027
            if (this.message.val().length === 0 && !(files && files[0]))
1028
            {
1029
                return;
1030
            }
1031
1032
            // if sjcl has not collected enough entropy yet, display a message
1033
            if (!sjcl.random.isReady())
1034
            {
1035
                this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
1036
                sjcl.random.addEventListener('seeded', function() {
1037
                    this.sendData(event);
1038
                });
1039
                return;
1040
            }
1041
1042
            $('.navbar-toggle').click();
1043
            this.password.addClass('hidden');
1044
            this.showStatus(i18n._('Sending paste...'), true);
1045
1046
            var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0),
1047
                password = this.passwordInput.val();
1048
            if(files && files[0])
1049
            {
1050
                if(typeof FileReader === undefined)
1051
                {
1052
                    this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
1053
                    return;
1054
                }
1055
                var reader = new FileReader();
1056
                // closure to capture the file information
1057
                reader.onload = (function(theFile)
1058
                {
1059
                    return function(e) {
1060
                        controller.sendDataContinue(
1061
                            randomkey,
1062
                            filter.cipher(randomkey, password, e.target.result),
1063
                            filter.cipher(randomkey, password, theFile.name)
1064
                        );
1065
                    };
1066
                })(files[0]);
1067
                reader.readAsDataURL(files[0]);
1068
            }
1069
            else if(this.attachmentLink.attr('href'))
1070
            {
1071
                this.sendDataContinue(
1072
                    randomkey,
1073
                    filter.cipher(randomkey, password, this.attachmentLink.attr('href')),
1074
                    this.attachmentLink.attr('download')
1075
                );
1076
            }
1077
            else
1078
            {
1079
                this.sendDataContinue(randomkey, '', '');
1080
            }
1081
        },
1082
1083
        /**
1084
         * send a new paste to server, step 2
1085
         *
1086
         * @name   controller.sendDataContinue
1087
         * @function
1088
         * @param  {string} randomkey
1089
         * @param  {string} cipherdata_attachment
1090
         * @param  {string} cipherdata_attachment_name
1091
         */
1092
        sendDataContinue: function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
1093
        {
1094
            var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val()),
1095
                data_to_send = {
1096
                    data:             cipherdata,
1097
                    expire:           $('#pasteExpiration').val(),
1098
                    formatter:        $('#pasteFormatter').val(),
1099
                    burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
1100
                    opendiscussion:   this.openDiscussion.is(':checked') ? 1 : 0
1101
                };
1102
            if (cipherdata_attachment.length > 0)
1103
            {
1104
                data_to_send.attachment = cipherdata_attachment;
1105
                if (cipherdata_attachment_name.length > 0)
1106
                {
1107
                    data_to_send.attachmentname = cipherdata_attachment_name;
1108
                }
1109
            }
1110
            $.ajax({
1111
                type: 'POST',
1112
                url: helper.scriptLocation(),
1113
                data: data_to_send,
1114
                dataType: 'json',
1115
                headers: this.headers,
1116
                success: function(data)
1117
                {
1118
                    if (data.status === 0) {
1119
                        controller.stateExistingPaste();
1120
                        var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey,
1121
                            deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
1122
                        controller.showStatus('');
1123
                        controller.errorMessage.addClass('hidden');
1124
                        history.pushState({type: 'newpaste'}, document.title, url);
1125
1126
                        $('#pastelink').html(
1127
                            i18n._(
1128
                                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1129
                                url, url
1130
                            ) + controller.shortenUrl(url)
1131
                        );
1132
                        var shortenButton = $('#shortenbutton');
1133
                        if (shortenButton) {
1134
                            shortenButton.click($.proxy(controller.sendToShortener, controller));
1135
                        }
1136
                        $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
1137
                        controller.pasteResult.removeClass('hidden');
1138
                        // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1139
                        helper.selectText('pasteurl');
1140
                        controller.showStatus('');
1141
                        controller.formatPaste(data_to_send.formatter, controller.message.val());
1142
                    }
1143
                    else if (data.status === 1)
1144
                    {
1145
                        controller.showError(i18n._('Could not create paste: %s', data.message));
1146
                    }
1147
                    else
1148
                    {
1149
                        controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
1150
                    }
1151
                }
1152
            })
1153
            .fail(function()
1154
            {
1155
                controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
1156
            });
1157
        },
1158
1159
        /**
1160
         * check if a URL shortener was defined and create HTML containing a link to it
1161
         *
1162
         * @name   controller.shortenUrl
1163
         * @function
1164
         * @param  {string} url
1165
         * @return {string} html
1166
         */
1167
        shortenUrl: function(url)
1168
        {
1169
            var shortenerHtml = $('#shortenbutton');
1170
            if (shortenerHtml) {
1171
                this.shortenerUrl = shortenerHtml.data('shortener');
1172
                this.createdPasteUrl = url;
1173
                return ' ' + $('<div />').append(shortenerHtml.clone()).html();
1174
            }
1175
            return '';
1176
        },
1177
1178
        /**
1179
         * put the screen in "New paste" mode
1180
         *
1181
         * @name   controller.stateNewPaste
1182
         * @function
1183
         */
1184
        stateNewPaste: function()
1185
        {
1186
            this.message.text('');
1187
            this.attachment.addClass('hidden');
1188
            this.cloneButton.addClass('hidden');
1189
            this.rawTextButton.addClass('hidden');
1190
            this.remainingTime.addClass('hidden');
1191
            this.pasteResult.addClass('hidden');
1192
            this.clearText.addClass('hidden');
1193
            this.discussion.addClass('hidden');
1194
            this.prettyMessage.addClass('hidden');
1195
            this.sendButton.removeClass('hidden');
1196
            this.expiration.removeClass('hidden');
1197
            this.formatter.removeClass('hidden');
1198
            this.burnAfterReadingOption.removeClass('hidden');
1199
            this.openDisc.removeClass('hidden');
1200
            this.newButton.removeClass('hidden');
1201
            this.password.removeClass('hidden');
1202
            this.attach.removeClass('hidden');
1203
            this.message.removeClass('hidden');
1204
            this.preview.removeClass('hidden');
1205
            this.message.focus();
1206
        },
1207
1208
        /**
1209
         * put the screen in "Existing paste" mode
1210
         *
1211
         * @name   controller.stateExistingPaste
1212
         * @function
1213
         * @param  {boolean} [preview=false] - (optional) tell if the preview tabs should be displayed, defaults to false
0 ignored issues
show
Documentation Bug introduced by
The parameter [preview=false] does not exist. Did you maybe mean preview instead?
Loading history...
1214
         */
1215
        stateExistingPaste: function(preview)
1216
        {
1217
            preview = preview || false;
1218
1219
            if (!preview)
1220
            {
1221
                // no "clone" for IE<10.
1222
                if ($('#oldienotice').is(":visible"))
1223
                {
1224
                    this.cloneButton.addClass('hidden');
1225
                }
1226
                else
1227
                {
1228
                    this.cloneButton.removeClass('hidden');
1229
                }
1230
1231
                this.rawTextButton.removeClass('hidden');
1232
                this.sendButton.addClass('hidden');
1233
                this.attach.addClass('hidden');
1234
                this.expiration.addClass('hidden');
1235
                this.formatter.addClass('hidden');
1236
                this.burnAfterReadingOption.addClass('hidden');
1237
                this.openDisc.addClass('hidden');
1238
                this.newButton.removeClass('hidden');
1239
                this.preview.addClass('hidden');
1240
            }
1241
1242
            this.pasteResult.addClass('hidden');
1243
            this.message.addClass('hidden');
1244
            this.clearText.addClass('hidden');
1245
            this.prettyMessage.addClass('hidden');
1246
        },
1247
1248
        /**
1249
         * when "burn after reading" is checked, disable discussion
1250
         *
1251
         * @name   controller.changeBurnAfterReading
1252
         * @function
1253
         */
1254
        changeBurnAfterReading: function()
1255
        {
1256
            if (this.burnAfterReading.is(':checked') )
1257
            {
1258
                this.openDisc.addClass('buttondisabled');
1259
                this.openDiscussion.attr({checked: false, disabled: true});
1260
            }
1261
            else
1262
            {
1263
                this.openDisc.removeClass('buttondisabled');
1264
                this.openDiscussion.removeAttr('disabled');
1265
            }
1266
        },
1267
1268
        /**
1269
         * when discussion is checked, disable "burn after reading"
1270
         *
1271
         * @name   controller.changeOpenDisc
1272
         * @function
1273
         */
1274
        changeOpenDisc: function()
1275
        {
1276
            if (this.openDiscussion.is(':checked') )
1277
            {
1278
                this.burnAfterReadingOption.addClass('buttondisabled');
1279
                this.burnAfterReading.attr({checked: false, disabled: true});
1280
            }
1281
            else
1282
            {
1283
                this.burnAfterReadingOption.removeClass('buttondisabled');
1284
                this.burnAfterReading.removeAttr('disabled');
1285
            }
1286
        },
1287
1288
        /**
1289
         * forward to URL shortener
1290
         *
1291
         * @name   controller.sendToShortener
1292
         * @function
1293
         * @param  {Event} event
1294
         */
1295
        sendToShortener: function(event)
1296
        {
1297
            event.preventDefault();
1298
            window.location.href = this.shortenerUrl + encodeURIComponent(this.createdPasteUrl);
1299
        },
1300
1301
        /**
1302
         * reload the page
1303
         *
1304
         * @name   controller.reloadPage
1305
         * @function
1306
         * @param  {Event} event
1307
         */
1308
        reloadPage: function(event)
1309
        {
1310
            event.preventDefault();
1311
            window.location.href = helper.scriptLocation();
1312
        },
1313
1314
        /**
1315
         * return raw text
1316
         *
1317
         * @name   controller.rawText
1318
         * @function
1319
         * @param  {Event} event
1320
         */
1321
        rawText: function(event)
1322
        {
1323
            event.preventDefault();
1324
            var paste = $('#pasteFormatter').val() === 'markdown' ?
1325
                this.prettyPrint.text() : this.clearText.text();
1326
            history.pushState(
1327
                null, document.title, helper.scriptLocation() + '?' +
1328
                helper.pasteId() + '#' + helper.pageKey()
1329
            );
1330
            // we use text/html instead of text/plain to avoid a bug when
1331
            // reloading the raw text view (it reverts to type text/html)
1332
            var newDoc = document.open('text/html', 'replace');
1333
            newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
1334
            newDoc.close();
1335
        },
1336
1337
        /**
1338
         * clone the current paste
1339
         *
1340
         * @name   controller.clonePaste
1341
         * @function
1342
         * @param  {Event} event
1343
         */
1344
        clonePaste: function(event)
1345
        {
1346
            event.preventDefault();
1347
            this.stateNewPaste();
1348
1349
            // erase the id and the key in url
1350
            history.replaceState(null, document.title, helper.scriptLocation());
1351
1352
            this.showStatus('');
1353
            if (this.attachmentLink.attr('href'))
1354
            {
1355
                this.clonedFile.removeClass('hidden');
1356
                this.fileWrap.addClass('hidden');
1357
            }
1358
            this.message.text(
1359
                $('#pasteFormatter').val() === 'markdown' ?
1360
                    this.prettyPrint.text() : this.clearText.text()
1361
            );
1362
            $('.navbar-toggle').click();
1363
        },
1364
1365
        /**
1366
         * set the expiration on bootstrap templates
1367
         *
1368
         * @name   controller.setExpiration
1369
         * @function
1370
         * @param  {Event} event
1371
         */
1372
        setExpiration: function(event)
1373
        {
1374
            event.preventDefault();
1375
            var target = $(event.target);
1376
            $('#pasteExpiration').val(target.data('expiration'));
1377
            $('#pasteExpirationDisplay').text(target.text());
1378
        },
1379
1380
        /**
1381
         * set the format on bootstrap templates
1382
         *
1383
         * @name   controller.setFormat
1384
         * @function
1385
         * @param  {Event} event
1386
         */
1387
        setFormat: function(event)
1388
        {
1389
            event.preventDefault();
1390
            var target = $(event.target);
1391
            $('#pasteFormatter').val(target.data('format'));
1392
            $('#pasteFormatterDisplay').text(target.text());
1393
1394
            if (this.messagePreview.parent().hasClass('active')) {
1395
                this.viewPreview(event);
1396
            }
1397
        },
1398
1399
        /**
1400
         * set the language in a cookie and reload the page
1401
         *
1402
         * @name   controller.setLanguage
1403
         * @function
1404
         * @param  {Event} event
1405
         */
1406
        setLanguage: function(event)
1407
        {
1408
            document.cookie = 'lang=' + $(event.target).data('lang');
1409
            this.reloadPage(event);
1410
        },
1411
1412
        /**
1413
         * support input of tab character
1414
         *
1415
         * @name   controller.supportTabs
1416
         * @function
1417
         * @param  {Event} event
1418
         */
1419
        supportTabs: function(event)
1420
        {
1421
            var keyCode = event.keyCode || event.which;
1422
            // tab was pressed
1423
            if (keyCode === 9)
1424
            {
1425
                // prevent the textarea to lose focus
1426
                event.preventDefault();
1427
                // get caret position & selection
1428
                var val   = this.value,
1429
                    start = this.selectionStart,
1430
                    end   = this.selectionEnd;
1431
                // set textarea value to: text before caret + tab + text after caret
1432
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1433
                // put caret at right position again
1434
                this.selectionStart = this.selectionEnd = start + 1;
1435
            }
1436
        },
1437
1438
        /**
1439
         * view the editor tab
1440
         *
1441
         * @name   controller.viewEditor
1442
         * @function
1443
         * @param  {Event} event
1444
         */
1445
        viewEditor: function(event)
1446
        {
1447
            event.preventDefault();
1448
            this.messagePreview.parent().removeClass('active');
1449
            this.messageEdit.parent().addClass('active');
1450
            this.message.focus();
1451
            this.stateNewPaste();
1452
        },
1453
1454
        /**
1455
         * view the preview tab
1456
         *
1457
         * @name   controller.viewPreview
1458
         * @function
1459
         * @param  {Event} event
1460
         */
1461
        viewPreview: function(event)
1462
        {
1463
            event.preventDefault();
1464
            this.messageEdit.parent().removeClass('active');
1465
            this.messagePreview.parent().addClass('active');
1466
            this.message.focus();
1467
            this.stateExistingPaste(true);
1468
            this.formatPaste($('#pasteFormatter').val(), this.message.val());
1469
        },
1470
1471
        /**
1472
         * handle history (pop) state changes
1473
         *
1474
         * currently this does only handle redirects to the home page.
1475
         *
1476
         * @name   controller.historyChange
1477
         * @function
1478
         * @param  {Event} event
1479
         */
1480
        historyChange: function(event)
1481
        {
1482
            var currentLocation = helper.scriptLocation();
1483
            if (event.originalEvent.state === null && // no state object passed
1484
                event.originalEvent.target.location.href === currentLocation && // target location is home page
1485
                window.location.href === currentLocation // and we are not already on the home page
1486
            ) {
1487
                // redirect to home page
1488
                window.location.href = currentLocation;
1489
            }
1490
        },
1491
1492
        /**
1493
         * create a new paste
1494
         *
1495
         * @name   controller.newPaste
1496
         * @function
1497
         */
1498
        newPaste: function()
1499
        {
1500
            this.stateNewPaste();
1501
            this.showStatus('');
1502
            this.message.text('');
1503
            this.changeBurnAfterReading();
1504
            this.changeOpenDisc();
1505
        },
1506
1507
        /**
1508
         * removes an attachment
1509
         *
1510
         * @name   controller.removeAttachment
1511
         * @function
1512
         */
1513
        removeAttachment: function()
1514
        {
1515
            this.clonedFile.addClass('hidden');
1516
            // removes the saved decrypted file data
1517
            this.attachmentLink.attr('href', '');
1518
            // the only way to deselect the file is to recreate the input
1519
            this.fileWrap.html(this.fileWrap.html());
1520
            this.fileWrap.removeClass('hidden');
1521
        },
1522
1523
        /**
1524
         * decrypt using the password from the modal dialog
1525
         *
1526
         * @name   controller.decryptPasswordModal
1527
         * @function
1528
         */
1529
        decryptPasswordModal: function()
1530
        {
1531
            this.passwordInput.val(this.passwordDecrypt.val());
1532
            this.displayMessages();
1533
        },
1534
1535
        /**
1536
         * submit a password in the modal dialog
1537
         *
1538
         * @name   controller.submitPasswordModal
1539
         * @function
1540
         * @param  {Event} event
1541
         */
1542
        submitPasswordModal: function(event)
1543
        {
1544
            event.preventDefault();
1545
            this.passwordModal.modal('hide');
1546
        },
1547
1548
        /**
1549
         * display an error message,
1550
         * we use the same function for paste and reply to comments
1551
         *
1552
         * @name   controller.showError
1553
         * @function
1554
         * @param  {string} message - text to display
1555
         */
1556
        showError: function(message)
1557
        {
1558
            if (this.status.length)
1559
            {
1560
                this.status.addClass('errorMessage').text(message);
1561
            }
1562
            else
1563
            {
1564
                this.errorMessage.removeClass('hidden');
1565
                helper.setMessage(this.errorMessage, message);
1566
            }
1567
            if (typeof this.replyStatus !== 'undefined') {
1568
                this.replyStatus.addClass('errorMessage');
1569
                this.replyStatus.addClass(this.errorMessage.attr('class'));
1570
                if (this.status.length)
1571
                {
1572
                    this.replyStatus.html(this.status.html());
1573
                }
1574
                else
1575
                {
1576
                    this.replyStatus.html(this.errorMessage.html());
1577
                }
1578
            }
1579
        },
1580
1581
        /**
1582
         * display a status message,
1583
         * we use the same function for paste and reply to comments
1584
         *
1585
         * @name   controller.showStatus
1586
         * @function
1587
         * @param  {string} message - text to display
1588
         * @param  {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
0 ignored issues
show
Documentation introduced by
The parameter [spin=false] does not exist. Did you maybe forget to remove this comment?
Loading history...
1589
         */
1590
        showStatus: function(message, spin)
1591
        {
1592
            if (spin || false)
1593
            {
1594
                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
1595
                this.status.prepend(img);
1596
                if (typeof this.replyStatus !== 'undefined') {
1597
                    this.replyStatus.prepend(img);
1598
                }
1599
            }
1600
            if (typeof this.replyStatus !== 'undefined') {
1601
                this.replyStatus.removeClass('errorMessage').text(message);
1602
            }
1603
            if (!message)
1604
            {
1605
                this.status.html(' ');
1606
                return;
1607
            }
1608
            if (message === '')
1609
            {
1610
                this.status.html(' ');
1611
                return;
1612
            }
1613
            this.status.removeClass('errorMessage').text(message);
1614
        },
1615
1616
        /**
1617
         * bind events to DOM elements
1618
         *
1619
         * @name   controller.bindEvents
1620
         * @function
1621
         */
1622
        bindEvents: function()
1623
        {
1624
            this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
1625
            this.openDisc.change($.proxy(this.changeOpenDisc, this));
1626
            this.sendButton.click($.proxy(this.sendData, this));
1627
            this.cloneButton.click($.proxy(this.clonePaste, this));
1628
            this.rawTextButton.click($.proxy(this.rawText, this));
1629
            this.fileRemoveButton.click($.proxy(this.removeAttachment, this));
1630
            $('.reloadlink').click($.proxy(this.reloadPage, this));
1631
            this.message.keydown(this.supportTabs);
1632
            this.messageEdit.click($.proxy(this.viewEditor, this));
1633
            this.messagePreview.click($.proxy(this.viewPreview, this));
1634
1635
            // bootstrap template drop downs
1636
            $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(this.setExpiration, this));
1637
            $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(this.setFormat, this));
1638
            $('#language ul.dropdown-menu li a').click($.proxy(this.setLanguage, this));
1639
1640
            // page template drop down
1641
            $('#language select option').click($.proxy(this.setLanguage, this));
1642
1643
            // handle modal password request on decryption
1644
            this.passwordModal.on('shown.bs.modal', $.proxy(this.passwordDecrypt.focus, this));
1645
            this.passwordModal.on('hidden.bs.modal', $.proxy(this.decryptPasswordModal, this));
1646
            this.passwordForm.submit($.proxy(this.submitPasswordModal, this));
1647
1648
            $(window).on('popstate', $.proxy(this.historyChange, this));
1649
        },
1650
1651
        /**
1652
         * main application
1653
         *
1654
         * @name   controller.init
1655
         * @function
1656
         */
1657
        init: function()
1658
        {
1659
            // hide "no javascript" message
1660
            $('#noscript').hide();
1661
1662
            // preload jQuery wrapped DOM elements and bind events
1663
            this.attach = $('#attach');
1664
            this.attachment = $('#attachment');
1665
            this.attachmentLink = $('#attachment a');
1666
            this.burnAfterReading = $('#burnafterreading');
1667
            this.burnAfterReadingOption = $('#burnafterreadingoption');
1668
            this.cipherData = $('#cipherdata');
1669
            this.clearText = $('#cleartext');
1670
            this.cloneButton = $('#clonebutton');
1671
            this.clonedFile = $('#clonedfile');
1672
            this.comments = $('#comments');
1673
            this.discussion = $('#discussion');
1674
            this.errorMessage = $('#errormessage');
1675
            this.expiration = $('#expiration');
1676
            this.fileRemoveButton = $('#fileremovebutton');
1677
            this.fileWrap = $('#filewrap');
1678
            this.formatter = $('#formatter');
1679
            this.image = $('#image');
1680
            this.message = $('#message');
1681
            this.messageEdit = $('#messageedit');
1682
            this.messagePreview = $('#messagepreview');
1683
            this.newButton = $('#newbutton');
1684
            this.openDisc = $('#opendisc');
1685
            this.openDiscussion = $('#opendiscussion');
1686
            this.password = $('#password');
1687
            this.passwordInput = $('#passwordinput');
1688
            this.passwordModal = $('#passwordmodal');
1689
            this.passwordForm = $('#passwordform');
1690
            this.passwordDecrypt = $('#passworddecrypt');
1691
            this.pasteResult = $('#pasteresult');
1692
            this.prettyMessage = $('#prettymessage');
1693
            this.prettyPrint = $('#prettyprint');
1694
            this.preview = $('#preview');
1695
            this.rawTextButton = $('#rawtextbutton');
1696
            this.remainingTime = $('#remainingtime');
1697
            this.sendButton = $('#sendbutton');
1698
            this.status = $('#status');
1699
            this.bindEvents();
1700
1701
            // display status returned by php code, if any (eg. paste was properly deleted)
1702
            if (this.status.text().length > 0)
1703
            {
1704
                this.showStatus(this.status.text());
1705
                return;
1706
            }
1707
1708
            // keep line height even if content empty
1709
            this.status.html(' ');
1710
1711
            // display an existing paste
1712
            if (this.cipherData.text().length > 1)
1713
            {
1714
                // missing decryption key in URL?
1715
                if (window.location.hash.length === 0)
1716
                {
1717
                    this.showError(i18n._('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)'));
1718
                    return;
1719
                }
1720
1721
                // show proper elements on screen
1722
                this.stateExistingPaste();
1723
                this.displayMessages();
1724
            }
1725
            // display error message from php code
1726
            else if (this.errorMessage.text().length > 1)
1727
            {
1728
                this.showError(this.errorMessage.text());
1729
            }
1730
            // create a new paste
1731
            else
1732
            {
1733
                this.newPaste();
1734
            }
1735
        }
1736
    }
1737
1738
    /**
1739
     * main application start, called when DOM is fully loaded and
1740
     * runs controller initalization after translations are loaded
1741
     */
1742
    $(i18n.loadTranslations);
1743
1744
    return {
1745
        helper: helper,
1746
        i18n: i18n,
1747
        filter: filter,
1748
        controller: controller
1749
    };
1750
}(jQuery, sjcl, Base64, RawDeflate);
1751