Completed
Push — master ( 5ad02a...5130d9 )
by rugk
04:04
created

controller.stateOnlyNewPaste   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 24
rs 8.9713
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.stateOnlyNewPaste();
785
                    this.showError(i18n._('Could not decrypt data (Wrong key?)'));
786
                    return;
787
                }
788
            }
789
790
            // display paste expiration / for your eyes only
791
            if (paste.meta.expire_date)
792
            {
793
                var expiration = helper.secondsToHuman(paste.meta.remaining_time),
794
                    expirationLabel = [
795
                        'This document will expire in %d ' + expiration[1] + '.',
796
                        'This document will expire in %d ' + expiration[1] + 's.'
797
                    ];
798
                helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0]));
799
                this.remainingTime.removeClass('foryoureyesonly')
800
                                  .removeClass('hidden');
801
            }
802
            if (paste.meta.burnafterreading)
803
            {
804
                // unfortunately many web servers don't support DELETE (and PUT) out of the box
805
                $.ajax({
806
                    type: 'POST',
807
                    url: helper.scriptLocation() + '?' + helper.pasteId(),
808
                    data: {deletetoken: 'burnafterreading'},
809
                    dataType: 'json',
810
                    headers: this.headers
811
                })
812
                .fail(function() {
813
                    controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
814
                });
815
                helper.setMessage(this.remainingTime, i18n._(
816
                    'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
817
                ));
818
                this.remainingTime.addClass('foryoureyesonly')
819
                                  .removeClass('hidden');
820
                // discourage cloning (as it can't really be prevented)
821
                this.cloneButton.addClass('hidden');
822
            }
823
824
            // if the discussion is opened on this paste, display it
825
            if (paste.meta.opendiscussion)
826
            {
827
                this.comments.html('');
828
829
                // iterate over comments
830
                for (var i = 0; i < paste.comments.length; ++i)
831
                {
832
                    var place = this.comments,
833
                        comment = paste.comments[i],
834
                        commenttext = filter.decipher(key, password, comment.data),
835
                        // if parent comment exists, display below (CSS will automatically shift it to the right)
836
                        cname = '#comment_' + comment.parentid,
837
                        divComment = $('<article><div class="comment" id="comment_' + comment.id
838
                                   + '"><div class="commentmeta"><span class="nickname"></span>'
839
                                   + '<span class="commentdate"></span></div>'
840
                                   + '<div class="commentdata"></div>'
841
                                   + '<button class="btn btn-default btn-sm">'
842
                                   + i18n._('Reply') + '</button></div></article>'),
843
                        divCommentData = divComment.find('div.commentdata');
844
845
                    // if the element exists in page
846
                    if ($(cname).length)
847
                    {
848
                        place = $(cname);
849
                    }
850
                    divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this));
851
                    helper.setElementText(divCommentData, commenttext);
852
                    helper.urls2links(divCommentData);
853
854
                    // try to get optional nickname
855
                    var nick = filter.decipher(key, password, comment.meta.nickname);
856
                    if (nick.length > 0)
857
                    {
858
                        divComment.find('span.nickname').text(nick);
859
                    }
860
                    else
861
                    {
862
                        divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
863
                    }
864
                    divComment.find('span.commentdate')
865
                              .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
866
                              .attr('title', 'CommentID: ' + comment.id);
867
868
                    // if an avatar is available, display it
869
                    if (comment.meta.vizhash)
870
                    {
871
                        divComment.find('span.nickname')
872
                                  .before(
873
                                    '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
874
                                    i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
875
                                  );
876
                    }
877
878
                    place.append(divComment);
879
                }
880
                var divComment = $(
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable divComment already seems to be declared on line 837. 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...
881
                    '<div class="comment"><button class="btn btn-default btn-sm">' +
882
                    i18n._('Add comment') + '</button></div>'
883
                );
884
                divComment.find('button').click({commentid: helper.pasteId()}, $.proxy(this.openReply, this));
885
                this.comments.append(divComment);
886
                this.discussion.removeClass('hidden');
887
            }
888
        },
889
890
        /**
891
         * open the comment entry when clicking the "Reply" button of a comment
892
         *
893
         * @name   controller.openReply
894
         * @function
895
         * @param  {Event} event
896
         */
897
        openReply: function(event)
898
        {
899
            event.preventDefault();
900
901
            // remove any other reply area
902
            $('div.reply').remove();
903
904
            var source = $(event.target),
905
                commentid = event.data.commentid,
906
                hint = i18n._('Optional nickname...'),
907
                reply = $(
908
                    '<div class="reply"><input type="text" id="nickname" ' +
909
                    'class="form-control" title="' + hint + '" placeholder="' +
910
                    hint + '" /><textarea id="replymessage" class="replymessage ' +
911
                    'form-control" cols="80" rows="7"></textarea><br />' +
912
                    '<div id="replystatus"></div><button id="replybutton" ' +
913
                    'class="btn btn-default btn-sm">' + i18n._('Post comment') +
914
                    '</button></div>'
915
                );
916
            reply.find('button').click(
917
                {parentid: commentid},
918
                $.proxy(this.sendComment, this)
919
            );
920
            source.after(reply);
921
            this.replyStatus = $('#replystatus');
922
            $('#replymessage').focus();
923
        },
924
925
        /**
926
         * send a reply in a discussion
927
         *
928
         * @name   controller.sendComment
929
         * @function
930
         * @param  {Event} event
931
         */
932
        sendComment: function(event)
933
        {
934
            event.preventDefault();
935
            this.errorMessage.addClass('hidden');
936
            // do not send if no data
937
            var replyMessage = $('#replymessage');
938
            if (replyMessage.val().length === 0)
939
            {
940
                return;
941
            }
942
943
            this.showStatus(i18n._('Sending comment...'), true);
944
            var parentid = event.data.parentid,
945
                key = helper.pageKey(),
946
                cipherdata = filter.cipher(key, this.passwordInput.val(), replyMessage.val()),
947
                ciphernickname = '',
948
                nick = $('#nickname').val();
949
            if (nick.length > 0)
950
            {
951
                ciphernickname = filter.cipher(key, this.passwordInput.val(), nick);
952
            }
953
            var data_to_send = {
954
                data:     cipherdata,
955
                parentid: parentid,
956
                pasteid:  helper.pasteId(),
957
                nickname: ciphernickname
958
            };
959
960
            $.ajax({
961
                type: 'POST',
962
                url: helper.scriptLocation(),
963
                data: data_to_send,
964
                dataType: 'json',
965
                headers: this.headers,
966
                success: function(data)
967
                {
968
                    if (data.status === 0)
969
                    {
970
                        controller.showStatus(i18n._('Comment posted.'));
971
                        $.ajax({
972
                            type: 'GET',
973
                            url: helper.scriptLocation() + '?' + helper.pasteId(),
974
                            dataType: 'json',
975
                            headers: controller.headers,
976
                            success: function(data)
977
                            {
978
                                if (data.status === 0)
979
                                {
980
                                    controller.displayMessages(data);
981
                                }
982
                                else if (data.status === 1)
983
                                {
984
                                    controller.showError(i18n._('Could not refresh display: %s', data.message));
985
                                }
986
                                else
987
                                {
988
                                    controller.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
989
                                }
990
                            }
991
                        })
992
                        .fail(function() {
993
                            controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
994
                        });
995
                    }
996
                    else if (data.status === 1)
997
                    {
998
                        controller.showError(i18n._('Could not post comment: %s', data.message));
999
                    }
1000
                    else
1001
                    {
1002
                        controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
1003
                    }
1004
                }
1005
            })
1006
            .fail(function() {
1007
                controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
1008
            });
1009
        },
1010
1011
        /**
1012
         * send a new paste to server
1013
         *
1014
         * @name   controller.sendData
1015
         * @function
1016
         * @param  {Event} event
1017
         */
1018
        sendData: function(event)
1019
        {
1020
            event.preventDefault();
1021
            var file = document.getElementById('file'),
1022
                files = (file && file.files) ? file.files : null; // FileList object
1023
1024
            // do not send if no data.
1025
            if (this.message.val().length === 0 && !(files && files[0]))
1026
            {
1027
                return;
1028
            }
1029
1030
            // if sjcl has not collected enough entropy yet, display a message
1031
            if (!sjcl.random.isReady())
1032
            {
1033
                this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
1034
                sjcl.random.addEventListener('seeded', function() {
1035
                    this.sendData(event);
1036
                });
1037
                return;
1038
            }
1039
1040
            $('.navbar-toggle').click();
1041
            this.password.addClass('hidden');
1042
            this.showStatus(i18n._('Sending paste...'), true);
1043
1044
            this.stateSubmittingPaste();
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
                    // revert loading status…
1053
                    this.stateNewPaste();
1054
                    this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
1055
                    return;
1056
                }
1057
                var reader = new FileReader();
1058
                // closure to capture the file information
1059
                reader.onload = (function(theFile)
1060
                {
1061
                    return function(e) {
1062
                        controller.sendDataContinue(
1063
                            randomkey,
1064
                            filter.cipher(randomkey, password, e.target.result),
1065
                            filter.cipher(randomkey, password, theFile.name)
1066
                        );
1067
                    };
1068
                })(files[0]);
1069
                reader.readAsDataURL(files[0]);
1070
            }
1071
            else if(this.attachmentLink.attr('href'))
1072
            {
1073
                this.sendDataContinue(
1074
                    randomkey,
1075
                    filter.cipher(randomkey, password, this.attachmentLink.attr('href')),
1076
                    this.attachmentLink.attr('download')
1077
                );
1078
            }
1079
            else
1080
            {
1081
                this.sendDataContinue(randomkey, '', '');
1082
            }
1083
        },
1084
1085
        /**
1086
         * send a new paste to server, step 2
1087
         *
1088
         * @name   controller.sendDataContinue
1089
         * @function
1090
         * @param  {string} randomkey
1091
         * @param  {string} cipherdata_attachment
1092
         * @param  {string} cipherdata_attachment_name
1093
         */
1094
        sendDataContinue: function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
1095
        {
1096
            var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val()),
1097
                data_to_send = {
1098
                    data:             cipherdata,
1099
                    expire:           $('#pasteExpiration').val(),
1100
                    formatter:        $('#pasteFormatter').val(),
1101
                    burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
1102
                    opendiscussion:   this.openDiscussion.is(':checked') ? 1 : 0
1103
                };
1104
            if (cipherdata_attachment.length > 0)
1105
            {
1106
                data_to_send.attachment = cipherdata_attachment;
1107
                if (cipherdata_attachment_name.length > 0)
1108
                {
1109
                    data_to_send.attachmentname = cipherdata_attachment_name;
1110
                }
1111
            }
1112
            $.ajax({
1113
                type: 'POST',
1114
                url: helper.scriptLocation(),
1115
                data: data_to_send,
1116
                dataType: 'json',
1117
                headers: this.headers,
1118
                success: function(data)
1119
                {
1120
                    if (data.status === 0) {
1121
                        controller.stateExistingPaste();
1122
                        var url = helper.scriptLocation() + '?' + data.id + '#' + randomkey,
1123
                            deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
1124
                        controller.showStatus('');
1125
                        controller.errorMessage.addClass('hidden');
1126
                        // show new URL in browser bar
1127
                        history.pushState({type: 'newpaste'}, document.title, url);
1128
1129
                        $('#pastelink').html(
1130
                            i18n._(
1131
                                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1132
                                url, url
1133
                            ) + controller.shortenUrl(url)
1134
                        );
1135
                        // save newly created element
1136
                        controller.pasteUrl = $('#pasteurl');
1137
                        // and add click event
1138
                        controller.pasteUrl.click($.proxy(controller.pasteLinkClick, controller));
1139
1140
                        var shortenButton = $('#shortenbutton');
1141
                        if (shortenButton) {
1142
                            shortenButton.click($.proxy(controller.sendToShortener, controller));
1143
                        }
1144
                        $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
1145
                        controller.pasteResult.removeClass('hidden');
1146
                        // we pre-select the link so that the user only has to [Ctrl]+[c] the link
1147
                        helper.selectText('pasteurl');
1148
                        controller.showStatus('');
1149
                        controller.formatPaste(data_to_send.formatter, controller.message.val());
1150
                    }
1151
                    else if (data.status === 1)
1152
                    {
1153
                        // revert loading status…
1154
                        controller.stateNewPaste();
1155
                        controller.showError(i18n._('Could not create paste: %s', data.message));
1156
                    }
1157
                    else
1158
                    {
1159
                        // revert loading status…
1160
                        controller.stateNewPaste();
1161
                        controller.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
1162
                    }
1163
                }
1164
            })
1165
            .fail(function()
1166
            {
1167
                // revert loading status…
1168
                this.stateNewPaste();
1169
                controller.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
1170
            });
1171
        },
1172
1173
        /**
1174
         * check if a URL shortener was defined and create HTML containing a link to it
1175
         *
1176
         * @name   controller.shortenUrl
1177
         * @function
1178
         * @param  {string} url
1179
         * @return {string} html
1180
         */
1181
        shortenUrl: function(url)
1182
        {
1183
            var shortenerHtml = $('#shortenbutton');
1184
            if (shortenerHtml) {
1185
                this.shortenerUrl = shortenerHtml.data('shortener');
1186
                this.createdPasteUrl = url;
1187
                return ' ' + $('<div />').append(shortenerHtml.clone()).html();
1188
            }
1189
            return '';
1190
        },
1191
1192
        /**
1193
         * put the screen in "New paste" mode
1194
         *
1195
         * @name   controller.stateNewPaste
1196
         * @function
1197
         */
1198
        stateNewPaste: function()
1199
        {
1200
            this.message.text('');
1201
            this.attachment.addClass('hidden');
1202
            this.cloneButton.addClass('hidden');
1203
            this.rawTextButton.addClass('hidden');
1204
            this.remainingTime.addClass('hidden');
1205
            this.pasteResult.addClass('hidden');
1206
            this.clearText.addClass('hidden');
1207
            this.discussion.addClass('hidden');
1208
            this.prettyMessage.addClass('hidden');
1209
            this.loadingIndicator.addClass('hidden');
1210
            this.sendButton.removeClass('hidden');
1211
            this.expiration.removeClass('hidden');
1212
            this.formatter.removeClass('hidden');
1213
            this.burnAfterReadingOption.removeClass('hidden');
1214
            this.openDisc.removeClass('hidden');
1215
            this.newButton.removeClass('hidden');
1216
            this.password.removeClass('hidden');
1217
            this.attach.removeClass('hidden');
1218
            this.message.removeClass('hidden');
1219
            this.preview.removeClass('hidden');
1220
            this.message.focus();
1221
        },
1222
1223
        /**
1224
         * put the screen in mode after submitting a paste
1225
         *
1226
         * @name   controller.stateSubmittingPaste
1227
         * @function
1228
         */
1229
        stateSubmittingPaste: function()
1230
        {
1231
            this.message.text('');
1232
            this.attachment.addClass('hidden');
1233
            this.cloneButton.addClass('hidden');
1234
            this.rawTextButton.addClass('hidden');
1235
            this.remainingTime.addClass('hidden');
1236
            this.pasteResult.addClass('hidden');
1237
            this.clearText.addClass('hidden');
1238
            this.discussion.addClass('hidden');
1239
            this.prettyMessage.addClass('hidden');
1240
            this.sendButton.addClass('hidden');
1241
            this.expiration.addClass('hidden');
1242
            this.formatter.addClass('hidden');
1243
            this.burnAfterReadingOption.addClass('hidden');
1244
            this.openDisc.addClass('hidden');
1245
            this.newButton.addClass('hidden');
1246
            this.password.addClass('hidden');
1247
            this.attach.addClass('hidden');
1248
            this.message.addClass('hidden');
1249
            this.preview.addClass('hidden');
1250
1251
            this.loadingIndicator.removeClass('hidden');
1252
        },
1253
1254
        /**
1255
         * put the screen in a state where the only option is to submit a
1256
         * new paste
1257
         *
1258
         * @name   controller.stateOnlyNewPaste
1259
         * @function
1260
         */
1261
        stateOnlyNewPaste: function()
1262
        {
1263
            this.message.text('');
1264
            this.attachment.addClass('hidden');
1265
            this.cloneButton.addClass('hidden');
1266
            this.rawTextButton.addClass('hidden');
1267
            this.remainingTime.addClass('hidden');
1268
            this.pasteResult.addClass('hidden');
1269
            this.clearText.addClass('hidden');
1270
            this.discussion.addClass('hidden');
1271
            this.prettyMessage.addClass('hidden');
1272
            this.sendButton.addClass('hidden');
1273
            this.expiration.addClass('hidden');
1274
            this.formatter.addClass('hidden');
1275
            this.burnAfterReadingOption.addClass('hidden');
1276
            this.openDisc.addClass('hidden');
1277
            this.password.addClass('hidden');
1278
            this.attach.addClass('hidden');
1279
            this.message.addClass('hidden');
1280
            this.preview.addClass('hidden');
1281
            this.loadingIndicator.addClass('hidden');
1282
1283
            this.newButton.removeClass('hidden');
1284
        },
1285
1286
        /**
1287
         * put the screen in "Existing paste" mode
1288
         *
1289
         * @name   controller.stateExistingPaste
1290
         * @function
1291
         * @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...
1292
         */
1293
        stateExistingPaste: function(preview)
1294
        {
1295
            preview = preview || false;
1296
1297
            if (!preview)
1298
            {
1299
                // no "clone" for IE<10.
1300
                if ($('#oldienotice').is(":visible"))
1301
                {
1302
                    this.cloneButton.addClass('hidden');
1303
                }
1304
                else
1305
                {
1306
                    this.cloneButton.removeClass('hidden');
1307
                }
1308
1309
                this.rawTextButton.removeClass('hidden');
1310
                this.sendButton.addClass('hidden');
1311
                this.attach.addClass('hidden');
1312
                this.expiration.addClass('hidden');
1313
                this.formatter.addClass('hidden');
1314
                this.burnAfterReadingOption.addClass('hidden');
1315
                this.openDisc.addClass('hidden');
1316
                this.newButton.removeClass('hidden');
1317
                this.preview.addClass('hidden');
1318
            }
1319
1320
            this.pasteResult.addClass('hidden');
1321
            this.message.addClass('hidden');
1322
            this.clearText.addClass('hidden');
1323
            this.prettyMessage.addClass('hidden');
1324
            this.loadingIndicator.addClass('hidden');
1325
        },
1326
1327
        /**
1328
         * when "burn after reading" is checked, disable discussion
1329
         *
1330
         * @name   controller.changeBurnAfterReading
1331
         * @function
1332
         */
1333
        changeBurnAfterReading: function()
1334
        {
1335
            if (this.burnAfterReading.is(':checked') )
1336
            {
1337
                this.openDisc.addClass('buttondisabled');
1338
                this.openDiscussion.attr({checked: false, disabled: true});
1339
            }
1340
            else
1341
            {
1342
                this.openDisc.removeClass('buttondisabled');
1343
                this.openDiscussion.removeAttr('disabled');
1344
            }
1345
        },
1346
1347
        /**
1348
         * when discussion is checked, disable "burn after reading"
1349
         *
1350
         * @name   controller.changeOpenDisc
1351
         * @function
1352
         */
1353
        changeOpenDisc: function()
1354
        {
1355
            if (this.openDiscussion.is(':checked') )
1356
            {
1357
                this.burnAfterReadingOption.addClass('buttondisabled');
1358
                this.burnAfterReading.attr({checked: false, disabled: true});
1359
            }
1360
            else
1361
            {
1362
                this.burnAfterReadingOption.removeClass('buttondisabled');
1363
                this.burnAfterReading.removeAttr('disabled');
1364
            }
1365
        },
1366
1367
        /**
1368
         * forward to URL shortener
1369
         *
1370
         * @name   controller.sendToShortener
1371
         * @function
1372
         * @param  {Event} event
1373
         */
1374
        sendToShortener: function(event)
1375
        {
1376
            event.preventDefault();
1377
            window.location.href = this.shortenerUrl + encodeURIComponent(this.createdPasteUrl);
1378
        },
1379
1380
        /**
1381
         * reload the page
1382
         *
1383
         * This takes the user to the PrivateBin home page.
1384
         *
1385
         * @name   controller.reloadPage
1386
         * @function
1387
         * @param  {Event} event
1388
         */
1389
        reloadPage: function(event)
1390
        {
1391
            event.preventDefault();
1392
            window.location.href = helper.scriptLocation();
1393
        },
1394
1395
        /**
1396
         * return raw text
1397
         *
1398
         * @name   controller.rawText
1399
         * @function
1400
         * @param  {Event} event
1401
         */
1402
        rawText: function(event)
1403
        {
1404
            event.preventDefault();
1405
            var paste = $('#pasteFormatter').val() === 'markdown' ?
1406
                this.prettyPrint.text() : this.clearText.text();
1407
            history.pushState(
1408
                null, document.title, helper.scriptLocation() + '?' +
1409
                helper.pasteId() + '#' + helper.pageKey()
1410
            );
1411
            // we use text/html instead of text/plain to avoid a bug when
1412
            // reloading the raw text view (it reverts to type text/html)
1413
            var newDoc = document.open('text/html', 'replace');
1414
            newDoc.write('<pre>' + helper.htmlEntities(paste) + '</pre>');
1415
            newDoc.close();
1416
        },
1417
1418
        /**
1419
         * clone the current paste
1420
         *
1421
         * @name   controller.clonePaste
1422
         * @function
1423
         * @param  {Event} event
1424
         */
1425
        clonePaste: function(event)
1426
        {
1427
            event.preventDefault();
1428
            this.stateNewPaste();
1429
1430
            // erase the id and the key in url
1431
            history.replaceState(null, document.title, helper.scriptLocation());
1432
1433
            this.showStatus('');
1434
            if (this.attachmentLink.attr('href'))
1435
            {
1436
                this.clonedFile.removeClass('hidden');
1437
                this.fileWrap.addClass('hidden');
1438
            }
1439
            this.message.text(
1440
                $('#pasteFormatter').val() === 'markdown' ?
1441
                    this.prettyPrint.text() : this.clearText.text()
1442
            );
1443
            $('.navbar-toggle').click();
1444
        },
1445
1446
        /**
1447
         * set the expiration on bootstrap templates
1448
         *
1449
         * @name   controller.setExpiration
1450
         * @function
1451
         * @param  {Event} event
1452
         */
1453
        setExpiration: function(event)
1454
        {
1455
            event.preventDefault();
1456
            var target = $(event.target);
1457
            $('#pasteExpiration').val(target.data('expiration'));
1458
            $('#pasteExpirationDisplay').text(target.text());
1459
        },
1460
1461
        /**
1462
         * set the format on bootstrap templates
1463
         *
1464
         * @name   controller.setFormat
1465
         * @function
1466
         * @param  {Event} event
1467
         */
1468
        setFormat: function(event)
1469
        {
1470
            event.preventDefault();
1471
            var target = $(event.target);
1472
            $('#pasteFormatter').val(target.data('format'));
1473
            $('#pasteFormatterDisplay').text(target.text());
1474
1475
            if (this.messagePreview.parent().hasClass('active')) {
1476
                this.viewPreview(event);
1477
            }
1478
        },
1479
1480
        /**
1481
         * set the language in a cookie and reload the page
1482
         *
1483
         * @name   controller.setLanguage
1484
         * @function
1485
         * @param  {Event} event
1486
         */
1487
        setLanguage: function(event)
1488
        {
1489
            document.cookie = 'lang=' + $(event.target).data('lang');
1490
            this.reloadPage(event);
1491
        },
1492
1493
        /**
1494
         * support input of tab character
1495
         *
1496
         * @name   controller.supportTabs
1497
         * @function
1498
         * @param  {Event} event
1499
         */
1500
        supportTabs: function(event)
1501
        {
1502
            var keyCode = event.keyCode || event.which;
1503
            // tab was pressed
1504
            if (keyCode === 9)
1505
            {
1506
                // prevent the textarea to lose focus
1507
                event.preventDefault();
1508
                // get caret position & selection
1509
                var val   = this.value,
1510
                    start = this.selectionStart,
1511
                    end   = this.selectionEnd;
1512
                // set textarea value to: text before caret + tab + text after caret
1513
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1514
                // put caret at right position again
1515
                this.selectionStart = this.selectionEnd = start + 1;
1516
            }
1517
        },
1518
1519
        /**
1520
         * view the editor tab
1521
         *
1522
         * @name   controller.viewEditor
1523
         * @function
1524
         * @param  {Event} event
1525
         */
1526
        viewEditor: function(event)
1527
        {
1528
            event.preventDefault();
1529
            this.messagePreview.parent().removeClass('active');
1530
            this.messageEdit.parent().addClass('active');
1531
            this.message.focus();
1532
            this.stateNewPaste();
1533
        },
1534
1535
        /**
1536
         * view the preview tab
1537
         *
1538
         * @name   controller.viewPreview
1539
         * @function
1540
         * @param  {Event} event
1541
         */
1542
        viewPreview: function(event)
1543
        {
1544
            event.preventDefault();
1545
            this.messageEdit.parent().removeClass('active');
1546
            this.messagePreview.parent().addClass('active');
1547
            this.message.focus();
1548
            this.stateExistingPaste(true);
1549
            this.formatPaste($('#pasteFormatter').val(), this.message.val());
1550
        },
1551
1552
        /**
1553
         * handle history (pop) state changes
1554
         *
1555
         * currently this does only handle redirects to the home page.
1556
         *
1557
         * @name   controller.historyChange
1558
         * @function
1559
         * @param  {Event} event
1560
         */
1561
        historyChange: function(event)
1562
        {
1563
            var currentLocation = helper.scriptLocation();
1564
            if (event.originalEvent.state === null && // no state object passed
1565
                event.originalEvent.target.location.href === currentLocation && // target location is home page
1566
                window.location.href === currentLocation // and we are not already on the home page
1567
            ) {
1568
                // redirect to home page
1569
                window.location.href = currentLocation;
1570
            }
1571
        },
1572
1573
        /**
1574
         * Forces opening the paste if the link does not do this automatically.
1575
         *
1576
         * This is necessary as browsers will not reload the page when it is
1577
         * already loaded (which is fake as it is set via history.pushState()).
1578
         *
1579
         * @name   controller.pasteLinkClick
1580
         * @function
1581
         * @param  {Event} event
1582
         */
1583
        pasteLinkClick: function(event)
0 ignored issues
show
Unused Code introduced by
The parameter event is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
1584
        {
1585
            // check if location is (already) shown in URL bar
1586
            if (window.location.href === this.pasteUrl.attr('href')) {
1587
                // if so we need to load link by reloading the current site
1588
                window.location.reload(true);
1589
            }
1590
        },
1591
1592
        /**
1593
         * create a new paste
1594
         *
1595
         * @name   controller.newPaste
1596
         * @function
1597
         */
1598
        newPaste: function()
1599
        {
1600
            this.stateNewPaste();
1601
            this.showStatus('');
1602
            this.message.text('');
1603
            this.changeBurnAfterReading();
1604
            this.changeOpenDisc();
1605
        },
1606
1607
        /**
1608
         * removes an attachment
1609
         *
1610
         * @name   controller.removeAttachment
1611
         * @function
1612
         */
1613
        removeAttachment: function()
1614
        {
1615
            this.clonedFile.addClass('hidden');
1616
            // removes the saved decrypted file data
1617
            this.attachmentLink.attr('href', '');
1618
            // the only way to deselect the file is to recreate the input
1619
            this.fileWrap.html(this.fileWrap.html());
1620
            this.fileWrap.removeClass('hidden');
1621
        },
1622
1623
        /**
1624
         * decrypt using the password from the modal dialog
1625
         *
1626
         * @name   controller.decryptPasswordModal
1627
         * @function
1628
         */
1629
        decryptPasswordModal: function()
1630
        {
1631
            this.passwordInput.val(this.passwordDecrypt.val());
1632
            this.displayMessages();
1633
        },
1634
1635
        /**
1636
         * submit a password in the modal dialog
1637
         *
1638
         * @name   controller.submitPasswordModal
1639
         * @function
1640
         * @param  {Event} event
1641
         */
1642
        submitPasswordModal: function(event)
1643
        {
1644
            event.preventDefault();
1645
            this.passwordModal.modal('hide');
1646
        },
1647
1648
        /**
1649
         * display an error message,
1650
         * we use the same function for paste and reply to comments
1651
         *
1652
         * @name   controller.showError
1653
         * @function
1654
         * @param  {string} message - text to display
1655
         */
1656
        showError: function(message)
1657
        {
1658
            if (this.status.length)
1659
            {
1660
                this.status.addClass('errorMessage').text(message);
1661
            }
1662
            else
1663
            {
1664
                this.errorMessage.removeClass('hidden');
1665
                helper.setMessage(this.errorMessage, message);
1666
            }
1667
            if (typeof this.replyStatus !== 'undefined') {
1668
                this.replyStatus.addClass('errorMessage');
1669
                this.replyStatus.addClass(this.errorMessage.attr('class'));
1670
                if (this.status.length)
1671
                {
1672
                    this.replyStatus.html(this.status.html());
1673
                }
1674
                else
1675
                {
1676
                    this.replyStatus.html(this.errorMessage.html());
1677
                }
1678
            }
1679
        },
1680
1681
        /**
1682
         * display a status message,
1683
         * we use the same function for paste and reply to comments
1684
         *
1685
         * @name   controller.showStatus
1686
         * @function
1687
         * @param  {string} message - text to display
1688
         * @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...
1689
         */
1690
        showStatus: function(message, spin)
1691
        {
1692
            if (spin || false)
1693
            {
1694
                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
1695
                this.status.prepend(img);
1696
                if (typeof this.replyStatus !== 'undefined') {
1697
                    this.replyStatus.prepend(img);
1698
                }
1699
            }
1700
            if (typeof this.replyStatus !== 'undefined') {
1701
                this.replyStatus.removeClass('errorMessage').text(message);
1702
            }
1703
            if (!message)
1704
            {
1705
                this.status.html(' ');
1706
                return;
1707
            }
1708
            if (message === '')
1709
            {
1710
                this.status.html(' ');
1711
                return;
1712
            }
1713
            this.status.removeClass('errorMessage').text(message);
1714
        },
1715
1716
        /**
1717
         * bind events to DOM elements
1718
         *
1719
         * @name   controller.bindEvents
1720
         * @function
1721
         */
1722
        bindEvents: function()
1723
        {
1724
            this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
1725
            this.openDisc.change($.proxy(this.changeOpenDisc, this));
1726
            this.sendButton.click($.proxy(this.sendData, this));
1727
            this.cloneButton.click($.proxy(this.clonePaste, this));
1728
            this.rawTextButton.click($.proxy(this.rawText, this));
1729
            this.fileRemoveButton.click($.proxy(this.removeAttachment, this));
1730
            $('.reloadlink').click($.proxy(this.reloadPage, this));
1731
            this.message.keydown(this.supportTabs);
1732
            this.messageEdit.click($.proxy(this.viewEditor, this));
1733
            this.messagePreview.click($.proxy(this.viewPreview, this));
1734
1735
            // bootstrap template drop downs
1736
            $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(this.setExpiration, this));
1737
            $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(this.setFormat, this));
1738
            $('#language ul.dropdown-menu li a').click($.proxy(this.setLanguage, this));
1739
1740
            // page template drop down
1741
            $('#language select option').click($.proxy(this.setLanguage, this));
1742
1743
            // handle modal password request on decryption
1744
            this.passwordModal.on('shown.bs.modal', $.proxy(this.passwordDecrypt.focus, this));
1745
            this.passwordModal.on('hidden.bs.modal', $.proxy(this.decryptPasswordModal, this));
1746
            this.passwordForm.submit($.proxy(this.submitPasswordModal, this));
1747
1748
            $(window).on('popstate', $.proxy(this.historyChange, this));
1749
        },
1750
1751
        /**
1752
         * main application
1753
         *
1754
         * @name   controller.init
1755
         * @function
1756
         */
1757
        init: function()
1758
        {
1759
            // hide "no javascript" message
1760
            $('#noscript').hide();
1761
1762
            // preload jQuery wrapped DOM elements and bind events
1763
            this.attach = $('#attach');
1764
            this.attachment = $('#attachment');
1765
            this.attachmentLink = $('#attachment a');
1766
            this.burnAfterReading = $('#burnafterreading');
1767
            this.burnAfterReadingOption = $('#burnafterreadingoption');
1768
            this.cipherData = $('#cipherdata');
1769
            this.clearText = $('#cleartext');
1770
            this.cloneButton = $('#clonebutton');
1771
            this.clonedFile = $('#clonedfile');
1772
            this.comments = $('#comments');
1773
            this.discussion = $('#discussion');
1774
            this.errorMessage = $('#errormessage');
1775
            this.expiration = $('#expiration');
1776
            this.fileRemoveButton = $('#fileremovebutton');
1777
            this.fileWrap = $('#filewrap');
1778
            this.formatter = $('#formatter');
1779
            this.image = $('#image');
1780
            this.loadingIndicator = $('#loadingindicator');
1781
            this.message = $('#message');
1782
            this.messageEdit = $('#messageedit');
1783
            this.messagePreview = $('#messagepreview');
1784
            this.newButton = $('#newbutton');
1785
            this.openDisc = $('#opendisc');
1786
            this.openDiscussion = $('#opendiscussion');
1787
            this.password = $('#password');
1788
            this.passwordInput = $('#passwordinput');
1789
            this.passwordModal = $('#passwordmodal');
1790
            this.passwordForm = $('#passwordform');
1791
            this.passwordDecrypt = $('#passworddecrypt');
1792
            this.pasteResult = $('#pasteresult');
1793
            // this.pasteUrl is saved in sendDataContinue() if/after it is
1794
            // actually created
1795
            this.prettyMessage = $('#prettymessage');
1796
            this.prettyPrint = $('#prettyprint');
1797
            this.preview = $('#preview');
1798
            this.rawTextButton = $('#rawtextbutton');
1799
            this.remainingTime = $('#remainingtime');
1800
            this.sendButton = $('#sendbutton');
1801
            this.status = $('#status');
1802
            this.bindEvents();
1803
1804
            // display status returned by php code, if any (eg. paste was properly deleted)
1805
            if (this.status.text().length > 0)
1806
            {
1807
                this.showStatus(this.status.text());
1808
                return;
1809
            }
1810
1811
            // keep line height even if content empty
1812
            this.status.html(' ');
1813
1814
            // display an existing paste
1815
            if (this.cipherData.text().length > 1)
1816
            {
1817
                // missing decryption key in URL?
1818
                if (window.location.hash.length === 0)
1819
                {
1820
                    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?)'));
1821
                    return;
1822
                }
1823
1824
                // show proper elements on screen
1825
                this.stateExistingPaste();
1826
                this.displayMessages();
1827
            }
1828
            // display error message from php code
1829
            else if (this.errorMessage.text().length > 1)
1830
            {
1831
                this.showError(this.errorMessage.text());
1832
            }
1833
            // create a new paste
1834
            else
1835
            {
1836
                this.newPaste();
1837
            }
1838
        }
1839
    }
1840
1841
    /**
1842
     * main application start, called when DOM is fully loaded and
1843
     * runs controller initalization after translations are loaded
1844
     */
1845
    $(i18n.loadTranslations);
1846
1847
    return {
1848
        helper: helper,
1849
        i18n: i18n,
1850
        filter: filter,
1851
        controller: controller
1852
    };
1853
}(jQuery, sjcl, Base64, RawDeflate);
1854