Completed
Push — master ( 90e0bf...3d6676 )
by El
03:01
created

js/privatebin.js (1 issue)

Upgrade to new PHP Analysis Engine

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

1
/**
2
 * PrivateBin
3
 *
4
 * a zero-knowledge paste bin
5
 *
6
 * @link      https://github.com/PrivateBin/PrivateBin
7
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
8
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
9
 * @version   1.0
10
 */
11
12
'use strict';
13
/** global: Base64 */
14
/** global: FileReader */
15
/** global: RawDeflate */
16
/** global: history */
17
/** global: navigator */
18
/** global: prettyPrint */
19
/** global: prettyPrintOne */
20
/** global: showdown */
21
/** global: sjcl */
22
23
// Immediately start random number generator collector.
24
sjcl.random.startCollectors();
25
26
$(function() {
27
    /**
28
     * static helper methods
29
     */
30
    var helper = {
31
        /**
32
         * Converts a duration (in seconds) into human friendly approximation.
33
         *
34
         * @param int seconds
35
         * @return array
36
         */
37
        secondsToHuman: function(seconds)
38
        {
39
            var v;
40
            if (seconds < 60)
41
            {
42
                v = Math.floor(seconds);
43
                return [v, 'second'];
44
            }
45
            if (seconds < 60 * 60)
46
            {
47
                v = Math.floor(seconds / 60);
48
                return [v, 'minute'];
49
            }
50
            if (seconds < 60 * 60 * 24)
51
            {
52
                v = Math.floor(seconds / (60 * 60));
53
                return [v, 'hour'];
54
            }
55
            // If less than 2 months, display in days:
56
            if (seconds < 60 * 60 * 24 * 60)
57
            {
58
                v = Math.floor(seconds / (60 * 60 * 24));
59
                return [v, 'day'];
60
            }
61
            v = Math.floor(seconds / (60 * 60 * 24 * 30));
62
            return [v, 'month'];
63
        },
64
65
        /**
66
         * Converts an associative array to an encoded string
67
         * for appending to the anchor.
68
         *
69
         * @param object associative_array Object to be serialized
70
         * @return string
71
         */
72
        hashToParameterString: function(associativeArray)
73
        {
74
            var parameterString = '';
75
            for (var key in associativeArray)
76
            {
77
                if(parameterString === '')
78
                {
79
                    parameterString = encodeURIComponent(key);
80
                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
81
                }
82
                else
83
                {
84
                    parameterString += '&' + encodeURIComponent(key);
85
                    parameterString += '=' + encodeURIComponent(associativeArray[key]);
86
                }
87
            }
88
            // padding for URL shorteners
89
            parameterString += '&p=p';
90
91
            return parameterString;
92
        },
93
94
        /**
95
         * Converts a string to an associative array.
96
         *
97
         * @param string parameter_string String containing parameters
98
         * @return object
99
         */
100
        parameterStringToHash: function(parameterString)
101
        {
102
            var parameterHash = {};
103
            var parameterArray = parameterString.split('&');
104
            for (var i = 0; i < parameterArray.length; i++)
105
            {
106
                var pair = parameterArray[i].split('=');
107
                var key = decodeURIComponent(pair[0]);
108
                var value = decodeURIComponent(pair[1]);
109
                parameterHash[key] = value;
110
            }
111
112
            return parameterHash;
113
        },
114
115
        /**
116
         * Get an associative array of the parameters found in the anchor
117
         *
118
         * @return object
119
         */
120
        getParameterHash: function()
121
        {
122
            var hashIndex = window.location.href.indexOf('#');
123
            if (hashIndex >= 0)
124
            {
125
                return this.parameterStringToHash(window.location.href.substring(hashIndex + 1));
126
            }
127
            else
128
            {
129
                return {};
130
            }
131
        },
132
133
        /**
134
         * Text range selection.
135
         * From: https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
136
         *
137
         * @param string element : Indentifier of the element to select (id="").
138
         */
139
        selectText: function(element)
140
        {
141
            var doc = document,
142
                text = doc.getElementById(element),
143
                range,
144
                selection;
145
146
            // MS
147
            if (doc.body.createTextRange)
148
            {
149
                range = doc.body.createTextRange();
150
                range.moveToElementText(text);
151
                range.select();
152
            }
153
            // all others
154
            else if (window.getSelection)
155
            {
156
                selection = window.getSelection();
157
                range = doc.createRange();
158
                range.selectNodeContents(text);
159
                selection.removeAllRanges();
160
                selection.addRange(range);
161
            }
162
        },
163
164
        /**
165
         * Set text of a DOM element (required for IE)
166
         * This is equivalent to element.text(text)
167
         *
168
         * @param object element : a DOM element.
169
         * @param string text : the text to enter.
170
         */
171
        setElementText: function(element, text)
172
        {
173
            // For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
174
            if ($('#oldienotice').is(':visible')) {
175
                var html = this.htmlEntities(text).replace(/\n/ig,'\r\n<br>');
176
                element.html('<pre>'+html+'</pre>');
177
            }
178
            // for other (sane) browsers:
179
            else
180
            {
181
                element.text(text);
182
            }
183
        },
184
185
        /**
186
         * replace last child of element with message
187
         *
188
         * @param object element : a jQuery wrapped DOM element.
189
         * @param string message : the message to append.
190
         */
191
        setMessage: function(element, message)
192
        {
193
            var content = element.contents();
194
            if (content.length > 0)
195
            {
196
                content[content.length - 1].nodeValue = ' ' + message;
197
            }
198
            else
199
            {
200
                this.setElementText(element, message);
201
            }
202
        },
203
204
        /**
205
         * Convert URLs to clickable links.
206
         * URLs to handle:
207
         * <code>
208
         *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
209
         *     http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
210
         *     http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
211
         * </code>
212
         *
213
         * @param object element : a jQuery DOM element.
214
         */
215
        urls2links: function(element)
216
        {
217
            var markup = '<a href="$1" rel="nofollow">$1</a>';
218
            element.html(
219
                element.html().replace(
220
                    /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
221
                    markup
222
                )
223
            );
224
            element.html(
225
                element.html().replace(
226
                    /((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
227
                    markup
228
                )
229
            );
230
        },
231
232
        /**
233
         * minimal sprintf emulation for %s and %d formats
234
         * From: https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914
235
         *
236
         * @param string format
237
         * @param mixed args one or multiple parameters injected into format string
238
         * @return string
239
         */
240
        sprintf: function()
241
        {
242
            var args = arguments;
243
            if (typeof arguments[0] === 'object')
244
            {
245
                args = arguments[0];
246
            }
247
            var string = args[0],
248
                i = 1;
249
            return string.replace(/%((%)|s|d)/g, function (m) {
250
                // m is the matched format, e.g. %s, %d
251
                var val;
252
                if (m[2]) {
253
                    val = m[2];
254
                } else {
255
                    val = args[i];
256
                    // A switch statement so that the formatter can be extended.
257
                    switch (m)
258
                    {
259
                        case '%d':
260
                            val = parseFloat(val);
261
                            if (isNaN(val)) {
262
                                val = 0;
263
                            }
264
                            break;
265
                        default:
266
                            // Default is %s
267
                    }
268
                    ++i;
269
                }
270
                return val;
271
            });
272
        },
273
274
        /**
275
         * get value of cookie, if it was set, empty string otherwise
276
         * From: http://www.w3schools.com/js/js_cookies.asp
277
         *
278
         * @param string cname
279
         * @return string
280
         */
281
        getCookie: function(cname) {
282
            var name = cname + '=';
283
            var ca = document.cookie.split(';');
284
            for (var i = 0; i < ca.length; ++i) {
285
                var c = ca[i];
286
                while (c.charAt(0) === ' ') c = c.substring(1);
287
                if (c.indexOf(name) === 0)
288
                {
289
                    return c.substring(name.length, c.length);
290
                }
291
            }
292
            return '';
293
        },
294
295
        /**
296
         * Convert all applicable characters to HTML entities.
297
         * From: https://github.com/janl/mustache.js/blob/master/mustache.js#L60
298
         *
299
         * @param string str
300
         * @return string escaped HTML
301
         */
302
        htmlEntities: function(str) {
303
            return String(str).replace(
304
                /[&<>"'`=\/]/g, function(s) {
305
                    return helper.entityMap[s];
306
                });
307
        },
308
309
        /**
310
         * character to HTML entity lookup table
311
         */
312
        entityMap: {
313
            '&': '&amp;',
314
            '<': '&lt;',
315
            '>': '&gt;',
316
            '"': '&quot;',
317
            "'": '&#39;',
318
            '/': '&#x2F;',
319
            '`': '&#x60;',
320
            '=': '&#x3D;'
321
        }
322
    };
323
324
    /**
325
     * internationalization methods
326
     */
327
    var i18n = {
328
        /**
329
         * supported languages, minus the built in 'en'
330
         */
331
        supportedLanguages: ['de', 'fr', 'it', 'pl', 'sl', 'zh'],
332
333
        /**
334
         * translate a string, alias for translate()
335
         *
336
         * @param string $messageId
337
         * @param mixed args one or multiple parameters injected into placeholders
338
         * @return string
339
         */
340
        _: function()
341
        {
342
            return this.translate(arguments);
343
        },
344
345
        /**
346
         * translate a string
347
         *
348
         * @param string $messageId
349
         * @param mixed args one or multiple parameters injected into placeholders
350
         * @return string
351
         */
352
        translate: function()
353
        {
354
            var args = arguments, messageId;
355
            if (typeof arguments[0] === 'object')
356
            {
357
                args = arguments[0];
358
            }
359
            var usesPlurals = $.isArray(args[0]);
360
            if (usesPlurals)
361
            {
362
                // use the first plural form as messageId, otherwise the singular
363
                messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
364
            }
365
            else
366
            {
367
                messageId = args[0];
368
            }
369
            if (messageId.length === 0)
370
            {
371
                return messageId;
372
            }
373
            if (!this.translations.hasOwnProperty(messageId))
374
            {
375
                if (this.language !== 'en')
376
                {
377
                    console.debug(
378
                        'Missing ' + this.language + ' translation for: ' + messageId
379
                    );
380
                }
381
                this.translations[messageId] = args[0];
382
            }
383
            if (usesPlurals && $.isArray(this.translations[messageId]))
384
            {
385
                var n = parseInt(args[1] || 1, 10),
386
                    key = this.getPluralForm(n),
387
                    maxKey = this.translations[messageId].length - 1;
388
                if (key > maxKey)
389
                {
390
                    key = maxKey;
391
                }
392
                args[0] = this.translations[messageId][key];
393
                args[1] = n;
394
            }
395
            else
396
            {
397
                args[0] = this.translations[messageId];
398
            }
399
            return helper.sprintf(args);
400
        },
401
402
        /**
403
         * per language functions to use to determine the plural form
404
         * From: http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
405
         *
406
         * @param int number
407
         * @return int array key
408
         */
409
        getPluralForm: function(n) {
410
            switch (this.language)
411
            {
412
                case 'fr':
413
                case 'zh':
414
                    return (n > 1 ? 1 : 0);
415
                case 'pl':
416
                    return (n === 1 ? 0 : n%10 >= 2 && n %10 <=4 && (n%100 < 10 || n%100 >= 20) ? 1 : 2);
417
                // en, de
418
                default:
419
                    return (n !== 1 ? 1 : 0);
420
            }
421
        },
422
423
        /**
424
         * load translations into cache, then execute callback function
425
         *
426
         * @param function callback
427
         */
428
        loadTranslations: function(callback)
429
        {
430
            var selectedLang = helper.getCookie('lang');
431
            var language = selectedLang.length > 0 ? selectedLang : (navigator.language || navigator.userLanguage).substring(0, 2);
432
            // note that 'en' is built in, so no translation is necessary
433
            if (this.supportedLanguages.indexOf(language) === -1)
434
            {
435
                callback();
436
            }
437
            else
438
            {
439
                $.getJSON('i18n/' + language + '.json', function(data) {
440
                    i18n.language = language;
441
                    i18n.translations = data;
442
                    callback();
443
                });
444
            }
445
        },
446
447
        /**
448
         * built in language
449
         */
450
        language: 'en',
451
452
        /**
453
         * translation cache
454
         */
455
        translations: {}
456
    };
457
458
    /**
459
     * filter methods
460
     */
461
    var filter = {
462
        /**
463
         * Compress a message (deflate compression). Returns base64 encoded data.
464
         *
465
         * @param string message
466
         * @return base64 string data
467
         */
468
        compress: function(message)
469
        {
470
            return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
471
        },
472
473
        /**
474
         * Decompress a message compressed with compress().
475
         *
476
         * @param base64 string data
477
         * @return string message
478
         */
479
        decompress: function(data)
480
        {
481
            return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
482
        },
483
484
        /**
485
         * Compress, then encrypt message with key.
486
         *
487
         * @param string key
488
         * @param string password
0 ignored issues
show
The parameter string has already been documented on line 487. The second definition is ignored.
Loading history...
489
         * @param string message
490
         * @return encrypted string data
491
         */
492
        cipher: function(key, password, message)
493
        {
494
            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
495
            var options = {mode: 'gcm', ks: 256, ts: 128};
496
            if ((password || '').trim().length === 0)
497
            {
498
                return sjcl.encrypt(key, this.compress(message), options);
499
            }
500
            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), this.compress(message), options);
501
        },
502
503
        /**
504
         * Decrypt message with key, then decompress.
505
         *
506
         * @param string key
507
         * @param string password
508
         * @param encrypted string data
509
         * @return string readable message
510
         */
511
        decipher: function(key, password, data)
512
        {
513
            if (data !== undefined)
514
            {
515
                try
516
                {
517
                    return this.decompress(sjcl.decrypt(key, data));
518
                }
519
                catch(err)
520
                {
521
                    try
522
                    {
523
                        return this.decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
524
                    }
525
                    catch(e)
526
                    {}
527
                }
528
            }
529
            return '';
530
        }
531
    };
532
533
    var privatebin = {
534
        /**
535
         * headers to send in AJAX requests
536
         */
537
        headers: {'X-Requested-With': 'JSONHttpRequest'},
538
539
        /**
540
         * URL shortners create address
541
         */
542
        shortenerUrl: '',
543
544
        /**
545
         * URL of newly created paste
546
         */
547
        createdPasteUrl: '',
548
549
        /**
550
         * Get the current script location (without search or hash part of the URL).
551
         * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
552
         *
553
         * @return string current script location
554
         */
555
        scriptLocation: function()
556
        {
557
            var scriptLocation = window.location.href.substring(0,window.location.href.length
558
                - window.location.search.length - window.location.hash.length),
559
                hashIndex = scriptLocation.indexOf('#');
560
            if (hashIndex !== -1)
561
            {
562
                scriptLocation = scriptLocation.substring(0, hashIndex);
563
            }
564
            return scriptLocation;
565
        },
566
567
        /**
568
         * Get the pastes unique identifier from the URL
569
         * eg. http://server.com/zero/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
570
         *
571
         * @return string unique identifier
572
         */
573
        pasteID: function()
574
        {
575
            return window.location.search.substring(1);
576
        },
577
578
        /**
579
         * Return the deciphering key stored in anchor part of the URL
580
         *
581
         * @return string key
582
         */
583
        pageKey: function()
584
        {
585
            // Some web 2.0 services and redirectors add data AFTER the anchor
586
            // (such as &utm_source=...). We will strip any additional data.
587
588
            var key = window.location.hash.substring(1),    // Get key
589
                i = key.indexOf('=');
590
591
            // First, strip everything after the equal sign (=) which signals end of base64 string.
592
            if (i > -1)
593
            {
594
                key = key.substring(0, i + 1);
595
            }
596
597
            // If the equal sign was not present, some parameters may remain:
598
            i = key.indexOf('&');
599
            if (i > -1)
600
            {
601
                key = key.substring(0, i);
602
            }
603
604
            // Then add trailing equal sign if it's missing
605
            if (key.charAt(key.length - 1) !== '=')
606
            {
607
                key += '=';
608
            }
609
610
            return key;
611
        },
612
613
        /**
614
         * ask the user for the password and set it
615
         */
616
        requestPassword: function()
617
        {
618
            if (this.passwordModal.length === 0) {
619
                var password = prompt(i18n._('Please enter the password for this paste:'), '');
620
                if (password === null)
621
                {
622
                    throw 'password prompt canceled';
623
                }
624
                if (password.length === 0)
625
                {
626
                    this.requestPassword();
627
                } else {
628
                    this.passwordInput.val(password);
629
                    this.displayMessages();
630
                }
631
            } else {
632
                this.passwordModal.modal();
633
            }
634
        },
635
636
        /**
637
         * use given format on paste, defaults to plain text
638
         *
639
         * @param string format
640
         * @param string text
641
         */
642
        formatPaste: function(format, text)
643
        {
644
            helper.setElementText(this.clearText, text);
645
            helper.setElementText(this.prettyPrint, text);
646
            switch (format || 'plaintext')
647
            {
648
                case 'markdown':
649
                    if (typeof showdown === 'object')
650
                    {
651
                        showdown.setOption('strikethrough', true);
652
                        showdown.setOption('tables', true);
653
                        showdown.setOption('tablesHeaderId', true);
654
                        var converter = new showdown.Converter();
655
                        this.clearText.html(
656
                            converter.makeHtml(text)
657
                        );
658
                        // add table classes from bootstrap css
659
                        this.clearText.find('table').addClass('table-condensed table-bordered');
660
661
                        this.clearText.removeClass('hidden');
662
                    }
663
                    this.prettyMessage.addClass('hidden');
664
                    break;
665
                case 'syntaxhighlighting':
666
                    if (typeof prettyPrintOne === 'function')
667
                    {
668
                        if (typeof prettyPrint === 'function')
669
                        {
670
                            prettyPrint();
671
                        }
672
                        this.prettyPrint.html(
673
                            prettyPrintOne(
674
                                helper.htmlEntities(text), null, true
675
                            )
676
                        );
677
                    }
678
                    // fall through, as the rest is the same
679
                default:
680
                    // Convert URLs to clickable links.
681
                    helper.urls2links(this.clearText);
682
                    helper.urls2links(this.prettyPrint);
683
                    this.clearText.addClass('hidden');
684
                    if (format === 'plaintext')
685
                    {
686
                        this.prettyPrint.css('white-space', 'pre-wrap');
687
                        this.prettyPrint.css('word-break', 'normal');
688
                        this.prettyPrint.removeClass('prettyprint');
689
                    }
690
                    this.prettyMessage.removeClass('hidden');
691
            }
692
        },
693
694
        /**
695
         * Show decrypted text in the display area, including discussion (if open)
696
         *
697
         * @param object paste (optional) object including comments to display (items = array with keys ('data','meta')
698
         */
699
        displayMessages: function(paste)
700
        {
701
            paste = paste || $.parseJSON(this.cipherData.text());
702
            var key = this.pageKey();
703
            var password = this.passwordInput.val();
704
            if (!this.prettyPrint.hasClass('prettyprinted')) {
705
                // Try to decrypt the paste.
706
                try
707
                {
708
                    if (paste.attachment)
709
                    {
710
                        var attachment = filter.decipher(key, password, paste.attachment);
711
                        if (attachment.length === 0)
712
                        {
713
                            if (password.length === 0)
714
                            {
715
                                this.requestPassword();
716
                                return;
717
                            }
718
                            attachment = filter.decipher(key, password, paste.attachment);
719
                        }
720
                        if (attachment.length === 0)
721
                        {
722
                            throw 'failed to decipher attachment';
723
                        }
724
725
                        if (paste.attachmentname)
726
                        {
727
                            var attachmentname = filter.decipher(key, password, paste.attachmentname);
728
                            if (attachmentname.length > 0)
729
                            {
730
                                this.attachmentLink.attr('download', attachmentname);
731
                            }
732
                        }
733
                        this.attachmentLink.attr('href', attachment);
734
                        this.attachment.removeClass('hidden');
735
736
                        // if the attachment is an image, display it
737
                        var imagePrefix = 'data:image/';
738
                        if (attachment.substring(0, imagePrefix.length) === imagePrefix)
739
                        {
740
                            this.image.html(
741
                                $(document.createElement('img'))
742
                                    .attr('src', attachment)
743
                                    .attr('class', 'img-thumbnail')
744
                            );
745
                            this.image.removeClass('hidden');
746
                        }
747
                    }
748
                    var cleartext = filter.decipher(key, password, paste.data);
749
                    if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
750
                    {
751
                        this.requestPassword();
752
                        return;
753
                    }
754
                    if (cleartext.length === 0 && !paste.attachment)
755
                    {
756
                        throw 'failed to decipher message';
757
                    }
758
759
                    this.passwordInput.val(password);
760
                    if (cleartext.length > 0)
761
                    {
762
                        $('#pasteFormatter').val(paste.meta.formatter);
763
                        this.formatPaste(paste.meta.formatter, cleartext);
764
                    }
765
                }
766
                catch(err)
767
                {
768
                    this.clearText.addClass('hidden');
769
                    this.prettyMessage.addClass('hidden');
770
                    this.cloneButton.addClass('hidden');
771
                    this.showError(i18n._('Could not decrypt data (Wrong key?)'));
772
                    return;
773
                }
774
            }
775
776
            // Display paste expiration / for your eyes only.
777
            if (paste.meta.expire_date)
778
            {
779
                var expiration = helper.secondsToHuman(paste.meta.remaining_time),
780
                    expirationLabel = [
781
                        'This document will expire in %d ' + expiration[1] + '.',
782
                        'This document will expire in %d ' + expiration[1] + 's.'
783
                    ];
784
                helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0]));
785
                this.remainingTime.removeClass('foryoureyesonly')
786
                                  .removeClass('hidden');
787
            }
788
            if (paste.meta.burnafterreading)
789
            {
790
                // unfortunately many web servers don't support DELETE (and PUT) out of the box
791
                $.ajax({
792
                    type: 'POST',
793
                    url: this.scriptLocation() + '?' + this.pasteID(),
794
                    data: {deletetoken: 'burnafterreading'},
795
                    dataType: 'json',
796
                    headers: this.headers
797
                })
798
                .fail(function() {
799
                    privatebin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
800
                });
801
                helper.setMessage(this.remainingTime, i18n._(
802
                    'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
803
                ));
804
                this.remainingTime.addClass('foryoureyesonly')
805
                                  .removeClass('hidden');
806
                // Discourage cloning (as it can't really be prevented).
807
                this.cloneButton.addClass('hidden');
808
            }
809
810
            // If the discussion is opened on this paste, display it.
811
            if (paste.meta.opendiscussion)
812
            {
813
                this.comments.html('');
814
815
                // iterate over comments
816
                for (var i = 0; i < paste.comments.length; ++i)
817
                {
818
                    var place = this.comments;
819
                    var comment = paste.comments[i];
820
                    var commenttext = filter.decipher(key, password, comment.data);
821
                    // If parent comment exists, display below (CSS will automatically shift it right.)
822
                    var cname = '#comment_' + comment.parentid;
823
824
                    // If the element exists in page
825
                    if ($(cname).length)
826
                    {
827
                        place = $(cname);
828
                    }
829
                    var divComment = $('<article><div class="comment" id="comment_' + comment.id + '">'
830
                                   + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
831
                                   + '<button class="btn btn-default btn-sm">' + i18n._('Reply') + '</button>'
832
                                   + '</div></article>');
833
                    divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this));
834
                    helper.setElementText(divComment.find('div.commentdata'), commenttext);
835
                    // Convert URLs to clickable links in comment.
836
                    helper.urls2links(divComment.find('div.commentdata'));
837
838
                    // Try to get optional nickname:
839
                    var nick = filter.decipher(key, password, comment.meta.nickname);
840
                    if (nick.length > 0)
841
                    {
842
                        divComment.find('span.nickname').text(nick);
843
                    }
844
                    else
845
                    {
846
                        divComment.find('span.nickname').html('<i>' + i18n._('Anonymous') + '</i>');
847
                    }
848
                    divComment.find('span.commentdate')
849
                              .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
850
                              .attr('title', 'CommentID: ' + comment.id);
851
852
                    // If an avatar is available, display it.
853
                    if (comment.meta.vizhash)
854
                    {
855
                        divComment.find('span.nickname')
856
                                  .before(
857
                                    '<img src="' + comment.meta.vizhash + '" class="vizhash" title="' +
858
                                    i18n._('Anonymous avatar (Vizhash of the IP address)') + '" /> '
859
                                  );
860
                    }
861
862
                    place.append(divComment);
863
                }
864
                var divComment = $(
865
                    '<div class="comment"><button class="btn btn-default btn-sm">' +
866
                    i18n._('Add comment') + '</button></div>'
867
                );
868
                divComment.find('button').click({commentid: this.pasteID()}, $.proxy(this.openReply, this));
869
                this.comments.append(divComment);
870
                this.discussion.removeClass('hidden');
871
            }
872
        },
873
874
        /**
875
         * Open the comment entry when clicking the "Reply" button of a comment.
876
         *
877
         * @param Event event
878
         */
879
        openReply: function(event)
880
        {
881
            event.preventDefault();
882
            var source = $(event.target),
883
                commentid = event.data.commentid,
884
                hint = i18n._('Optional nickname...');
885
886
            // Remove any other reply area.
887
            $('div.reply').remove();
888
            var reply = $(
889
                '<div class="reply">' +
890
                '<input type="text" id="nickname" class="form-control" title="' + hint + '" placeholder="' + hint + '" />' +
891
                '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>' +
892
                '<br /><div id="replystatus"></div><button id="replybutton" class="btn btn-default btn-sm">' +
893
                i18n._('Post comment') + '</button></div>'
894
            );
895
            reply.find('button').click({parentid: commentid}, $.proxy(this.sendComment, this));
896
            source.after(reply);
897
            this.replyStatus = $('#replystatus');
898
            $('#replymessage').focus();
899
        },
900
901
        /**
902
         * Send a reply in a discussion.
903
         *
904
         * @param Event event
905
         */
906
        sendComment: function(event)
907
        {
908
            event.preventDefault();
909
            this.errorMessage.addClass('hidden');
910
            // Do not send if no data.
911
            var replyMessage = $('#replymessage');
912
            if (replyMessage.val().length === 0)
913
            {
914
                return;
915
            }
916
917
            this.showStatus(i18n._('Sending comment...'), true);
918
            var parentid = event.data.parentid;
919
            var cipherdata = filter.cipher(this.pageKey(), this.passwordInput.val(), replyMessage.val());
920
            var ciphernickname = '';
921
            var nick = $('#nickname').val();
922
            if (nick !== '')
923
            {
924
                ciphernickname = filter.cipher(this.pageKey(), this.passwordInput.val(), nick);
925
            }
926
            var data_to_send = {
927
                data:     cipherdata,
928
                parentid: parentid,
929
                pasteid:  this.pasteID(),
930
                nickname: ciphernickname
931
            };
932
933
            $.ajax({
934
                type: 'POST',
935
                url: this.scriptLocation(),
936
                data: data_to_send,
937
                dataType: 'json',
938
                headers: this.headers,
939
                success: function(data)
940
                {
941
                    if (data.status === 0)
942
                    {
943
                        privatebin.showStatus(i18n._('Comment posted.'), false);
944
                        $.ajax({
945
                            type: 'GET',
946
                            url: privatebin.scriptLocation() + '?' + privatebin.pasteID(),
947
                            dataType: 'json',
948
                            headers: privatebin.headers,
949
                            success: function(data)
950
                            {
951
                                if (data.status === 0)
952
                                {
953
                                    privatebin.displayMessages(data);
954
                                }
955
                                else if (data.status === 1)
956
                                {
957
                                    privatebin.showError(i18n._('Could not refresh display: %s', data.message));
958
                                }
959
                                else
960
                                {
961
                                    privatebin.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
962
                                }
963
                            }
964
                        })
965
                        .fail(function() {
966
                            privatebin.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
967
                        });
968
                    }
969
                    else if (data.status === 1)
970
                    {
971
                        privatebin.showError(i18n._('Could not post comment: %s', data.message));
972
                    }
973
                    else
974
                    {
975
                        privatebin.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
976
                    }
977
                }
978
            })
979
            .fail(function() {
980
                privatebin.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
981
            });
982
        },
983
984
        /**
985
         * Send a new paste to server
986
         *
987
         * @param Event event
988
         */
989
        sendData: function(event)
990
        {
991
            event.preventDefault();
992
            var file = document.getElementById('file'),
993
                files = (file && file.files) ? file.files : null; // FileList object
994
995
            // Do not send if no data.
996
            if (this.message.val().length === 0 && !(files && files[0]))
997
            {
998
                return;
999
            }
1000
1001
            // If sjcl has not collected enough entropy yet, display a message.
1002
            if (!sjcl.random.isReady())
1003
            {
1004
                this.showStatus(i18n._('Sending paste (Please move your mouse for more entropy)...'), true);
1005
                sjcl.random.addEventListener('seeded', function() {
1006
                    this.sendData(event);
1007
                });
1008
                return;
1009
            }
1010
1011
            $('.navbar-toggle').click();
1012
            this.password.addClass('hidden');
1013
            this.showStatus(i18n._('Sending paste...'), true);
1014
1015
            var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
1016
            var password = this.passwordInput.val();
1017
            if(files && files[0])
1018
            {
1019
                if(typeof FileReader === undefined)
1020
                {
1021
                    this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.'));
1022
                    return;
1023
                }
1024
                var reader = new FileReader();
1025
                // Closure to capture the file information.
1026
                reader.onload = (function(theFile)
1027
                {
1028
                    return function(e) {
1029
                        privatebin.sendDataContinue(
1030
                            randomkey,
1031
                            filter.cipher(randomkey, password, e.target.result),
1032
                            filter.cipher(randomkey, password, theFile.name)
1033
                        );
1034
                    };
1035
                })(files[0]);
1036
                reader.readAsDataURL(files[0]);
1037
            }
1038
            else if(this.attachmentLink.attr('href'))
1039
            {
1040
                this.sendDataContinue(
1041
                    randomkey,
1042
                    filter.cipher(randomkey, password, this.attachmentLink.attr('href')),
1043
                    this.attachmentLink.attr('download')
1044
                );
1045
            }
1046
            else
1047
            {
1048
                this.sendDataContinue(randomkey, '', '');
1049
            }
1050
        },
1051
1052
        /**
1053
         * Send a new paste to server, step 2
1054
         *
1055
         * @param string randomkey
1056
         * @param encrypted string cipherdata_attachment
1057
         * @param encrypted string cipherdata_attachment_name
1058
         */
1059
        sendDataContinue: function(randomkey, cipherdata_attachment, cipherdata_attachment_name)
1060
        {
1061
            var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val());
1062
            var data_to_send = {
1063
                data:             cipherdata,
1064
                expire:           $('#pasteExpiration').val(),
1065
                formatter:        $('#pasteFormatter').val(),
1066
                burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
1067
                opendiscussion:   this.openDiscussion.is(':checked') ? 1 : 0
1068
            };
1069
            if (cipherdata_attachment.length > 0)
1070
            {
1071
                data_to_send.attachment = cipherdata_attachment;
1072
                if (cipherdata_attachment_name.length > 0)
1073
                {
1074
                    data_to_send.attachmentname = cipherdata_attachment_name;
1075
                }
1076
            }
1077
            $.ajax({
1078
                type: 'POST',
1079
                url: this.scriptLocation(),
1080
                data: data_to_send,
1081
                dataType: 'json',
1082
                headers: this.headers,
1083
                success: function(data)
1084
                {
1085
                    if (data.status === 0) {
1086
                        privatebin.stateExistingPaste();
1087
                        var url = privatebin.scriptLocation() + '?' + data.id + '#' + randomkey;
1088
                        var deleteUrl = privatebin.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
1089
                        privatebin.showStatus('', false);
1090
                        privatebin.errorMessage.addClass('hidden');
1091
1092
                        $('#pastelink').html(
1093
                            i18n._(
1094
                                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
1095
                                url, url
1096
                            ) + privatebin.shortenUrl(url)
1097
                        );
1098
                        var shortenButton = $('#shortenbutton');
1099
                        if (shortenButton) {
1100
                            shortenButton.click($.proxy(privatebin.sendToShortener, privatebin));
1101
                        }
1102
                        $('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
1103
                        privatebin.pasteResult.removeClass('hidden');
1104
                        // We pre-select the link so that the user only has to [Ctrl]+[c] the link.
1105
                        helper.selectText('pasteurl');
1106
                        privatebin.showStatus('', false);
1107
                        privatebin.formatPaste(data_to_send.formatter, privatebin.message.val());
1108
                    }
1109
                    else if (data.status === 1)
1110
                    {
1111
                        privatebin.showError(i18n._('Could not create paste: %s', data.message));
1112
                    }
1113
                    else
1114
                    {
1115
                        privatebin.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
1116
                    }
1117
                }
1118
            })
1119
            .fail(function()
1120
            {
1121
                privatebin.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
1122
            });
1123
        },
1124
1125
        /**
1126
         * Check if a URL shortener was defined and create HTML containing a link to it.
1127
         *
1128
         * @param string url
1129
         * @return string html
1130
         */
1131
        shortenUrl: function(url)
1132
        {
1133
            var shortenerHtml = $('#shortenbutton');
1134
            if (shortenerHtml) {
1135
                this.shortenerUrl = shortenerHtml.data('shortener');
1136
                this.createdPasteUrl = url;
1137
                return ' ' + $('<div />').append(shortenerHtml.clone()).html();
1138
            }
1139
            return '';
1140
        },
1141
1142
        /**
1143
         * Put the screen in "New paste" mode.
1144
         */
1145
        stateNewPaste: function()
1146
        {
1147
            this.message.text('');
1148
            this.attachment.addClass('hidden');
1149
            this.cloneButton.addClass('hidden');
1150
            this.rawTextButton.addClass('hidden');
1151
            this.remainingTime.addClass('hidden');
1152
            this.pasteResult.addClass('hidden');
1153
            this.clearText.addClass('hidden');
1154
            this.discussion.addClass('hidden');
1155
            this.prettyMessage.addClass('hidden');
1156
            this.sendButton.removeClass('hidden');
1157
            this.expiration.removeClass('hidden');
1158
            this.formatter.removeClass('hidden');
1159
            this.burnAfterReadingOption.removeClass('hidden');
1160
            this.openDisc.removeClass('hidden');
1161
            this.newButton.removeClass('hidden');
1162
            this.password.removeClass('hidden');
1163
            this.attach.removeClass('hidden');
1164
            this.message.removeClass('hidden');
1165
            this.preview.removeClass('hidden');
1166
            this.message.focus();
1167
        },
1168
1169
        /**
1170
         * Put the screen in "Existing paste" mode.
1171
         *
1172
         * @param boolean preview (optional) tell if the preview tabs should be displayed, defaults to false.
1173
         */
1174
        stateExistingPaste: function(preview)
1175
        {
1176
            preview = preview || false;
1177
1178
            if (!preview)
1179
            {
1180
                // No "clone" for IE<10.
1181
                if ($('#oldienotice').is(":visible"))
1182
                {
1183
                    this.cloneButton.addClass('hidden');
1184
                }
1185
                else
1186
                {
1187
                    this.cloneButton.removeClass('hidden');
1188
                }
1189
1190
                this.rawTextButton.removeClass('hidden');
1191
                this.sendButton.addClass('hidden');
1192
                this.attach.addClass('hidden');
1193
                this.expiration.addClass('hidden');
1194
                this.formatter.addClass('hidden');
1195
                this.burnAfterReadingOption.addClass('hidden');
1196
                this.openDisc.addClass('hidden');
1197
                this.newButton.removeClass('hidden');
1198
                this.preview.addClass('hidden');
1199
            }
1200
1201
            this.pasteResult.addClass('hidden');
1202
            this.message.addClass('hidden');
1203
            this.clearText.addClass('hidden');
1204
            this.prettyMessage.addClass('hidden');
1205
        },
1206
1207
        /**
1208
         * If "burn after reading" is checked, disable discussion.
1209
         */
1210
        changeBurnAfterReading: function()
1211
        {
1212
            if (this.burnAfterReading.is(':checked') )
1213
            {
1214
                this.openDisc.addClass('buttondisabled');
1215
                this.openDiscussion.attr({checked: false, disabled: true});
1216
            }
1217
            else
1218
            {
1219
                this.openDisc.removeClass('buttondisabled');
1220
                this.openDiscussion.removeAttr('disabled');
1221
            }
1222
        },
1223
1224
        /**
1225
         * If discussion is checked, disable "burn after reading".
1226
         */
1227
        changeOpenDisc: function()
1228
        {
1229
            if (this.openDiscussion.is(':checked') )
1230
            {
1231
                this.burnAfterReadingOption.addClass('buttondisabled');
1232
                this.burnAfterReading.attr({checked: false, disabled: true});
1233
            }
1234
            else
1235
            {
1236
                this.burnAfterReadingOption.removeClass('buttondisabled');
1237
                this.burnAfterReading.removeAttr('disabled');
1238
            }
1239
        },
1240
1241
        /**
1242
         * Forward to URL shortener.
1243
         *
1244
         * @param Event event
1245
         */
1246
        sendToShortener: function(event)
1247
        {
1248
            event.preventDefault();
1249
            window.location.href = this.shortenerUrl + encodeURIComponent(this.createdPasteUrl);
1250
        },
1251
1252
        /**
1253
         * Reload the page.
1254
         *
1255
         * @param Event event
1256
         */
1257
        reloadPage: function(event)
1258
        {
1259
            event.preventDefault();
1260
            window.location.href = this.scriptLocation();
1261
        },
1262
1263
        /**
1264
         * Return raw text.
1265
         *
1266
         * @param Event event
1267
         */
1268
        rawText: function(event)
1269
        {
1270
            event.preventDefault();
1271
            var paste = $('#pasteFormatter').val() === 'markdown' ?
1272
                this.prettyPrint.text() : this.clearText.text();
1273
            history.pushState(
1274
                null, document.title, this.scriptLocation() + '?' +
1275
                this.pasteID() + '#' + this.pageKey()
1276
            );
1277
            // we use text/html instead of text/plain to avoid a bug when
1278
            // reloading the raw text view (it reverts to type text/html)
1279
            var newDoc = document.open('text/html', 'replace');
1280
            newDoc.write('<pre>' + paste + '</pre>');
1281
            newDoc.close();
1282
        },
1283
1284
        /**
1285
         * Clone the current paste.
1286
         *
1287
         * @param Event event
1288
         */
1289
        clonePaste: function(event)
1290
        {
1291
            event.preventDefault();
1292
            this.stateNewPaste();
1293
1294
            // Erase the id and the key in url
1295
            history.replaceState(null, document.title, this.scriptLocation());
1296
1297
            this.showStatus('', false);
1298
            if (this.attachmentLink.attr('href'))
1299
            {
1300
                this.clonedFile.removeClass('hidden');
1301
                this.fileWrap.addClass('hidden');
1302
            }
1303
            this.message.text(
1304
                $('#pasteFormatter').val() === 'markdown' ?
1305
                    this.prettyPrint.text() : this.clearText.text()
1306
            );
1307
            $('.navbar-toggle').click();
1308
        },
1309
1310
        /**
1311
         * Set the expiration on bootstrap templates.
1312
         *
1313
         * @param Event event
1314
         */
1315
        setExpiration: function(event)
1316
        {
1317
            event.preventDefault();
1318
            var target = $(event.target);
1319
            $('#pasteExpiration').val(target.data('expiration'));
1320
            $('#pasteExpirationDisplay').text(target.text());
1321
        },
1322
1323
        /**
1324
         * Set the format on bootstrap templates.
1325
         *
1326
         * @param Event event
1327
         */
1328
        setFormat: function(event)
1329
        {
1330
            event.preventDefault();
1331
            var target = $(event.target);
1332
            $('#pasteFormatter').val(target.data('format'));
1333
            $('#pasteFormatterDisplay').text(target.text());
1334
1335
            if (this.messagePreview.parent().hasClass('active')) {
1336
                this.viewPreview(event);
1337
            }
1338
        },
1339
1340
        /**
1341
         * Set the language on bootstrap templates.
1342
         *
1343
         * Sets the language cookie and reloads the page.
1344
         *
1345
         * @param Event event
1346
         */
1347
        setLanguage: function(event)
1348
        {
1349
            document.cookie = 'lang=' + $(event.target).data('lang');
1350
            this.reloadPage(event);
1351
        },
1352
1353
        /**
1354
         * Support input of tab character.
1355
         *
1356
         * @param Event event
1357
         */
1358
        supportTabs: function(event)
1359
        {
1360
            var keyCode = event.keyCode || event.which;
1361
            // tab was pressed
1362
            if (keyCode === 9)
1363
            {
1364
                // prevent the textarea to lose focus
1365
                event.preventDefault();
1366
                // get caret position & selection
1367
                var val   = this.value,
1368
                    start = this.selectionStart,
1369
                    end   = this.selectionEnd;
1370
                // set textarea value to: text before caret + tab + text after caret
1371
                this.value = val.substring(0, start) + '\t' + val.substring(end);
1372
                // put caret at right position again
1373
                this.selectionStart = this.selectionEnd = start + 1;
1374
            }
1375
        },
1376
1377
        /**
1378
         * View the editor tab.
1379
         *
1380
         * @param Event event
1381
         */
1382
        viewEditor: function(event)
1383
        {
1384
            event.preventDefault();
1385
            this.messagePreview.parent().removeClass('active');
1386
            this.messageEdit.parent().addClass('active');
1387
            this.message.focus();
1388
            this.stateNewPaste();
1389
        },
1390
1391
        /**
1392
         * View the preview tab.
1393
         *
1394
         * @param Event event
1395
         */
1396
        viewPreview: function(event)
1397
        {
1398
            event.preventDefault();
1399
            this.messageEdit.parent().removeClass('active');
1400
            this.messagePreview.parent().addClass('active');
1401
            this.message.focus();
1402
            this.stateExistingPaste(true);
1403
            this.formatPaste($('#pasteFormatter').val(), this.message.val());
1404
        },
1405
1406
        /**
1407
         * Create a new paste.
1408
         */
1409
        newPaste: function()
1410
        {
1411
            this.stateNewPaste();
1412
            this.showStatus('', false);
1413
            this.message.text('');
1414
            this.changeBurnAfterReading();
1415
            this.changeOpenDisc();
1416
        },
1417
1418
        /**
1419
         * Removes an attachment.
1420
         */
1421
        removeAttachment: function()
1422
        {
1423
            this.clonedFile.addClass('hidden');
1424
            // removes the saved decrypted file data
1425
            this.attachmentLink.attr('href', '');
1426
            // the only way to deselect the file is to recreate the input
1427
            this.fileWrap.html(this.fileWrap.html());
1428
            this.fileWrap.removeClass('hidden');
1429
        },
1430
1431
        /**
1432
         * Focus on the modal password dialog.
1433
         */
1434
        focusPasswordModal: function()
1435
        {
1436
            this.passwordDecrypt.focus();
1437
        },
1438
1439
        /**
1440
         * Decrypt using the password from the modal dialog.
1441
         */
1442
        decryptPasswordModal: function()
1443
        {
1444
            this.passwordInput.val(this.passwordDecrypt.val());
1445
            this.displayMessages();
1446
        },
1447
1448
        /**
1449
         * Submit a password in the modal dialog.
1450
         *
1451
         * @param Event event
1452
         */
1453
        submitPasswordModal: function(event)
1454
        {
1455
            event.preventDefault();
1456
            this.passwordModal.modal('hide');
1457
        },
1458
1459
        /**
1460
         * Display an error message
1461
         * (We use the same function for paste and reply to comments)
1462
         *
1463
         * @param string message : text to display
1464
         */
1465
        showError: function(message)
1466
        {
1467
            if (this.status.length)
1468
            {
1469
                this.status.addClass('errorMessage').text(message);
1470
            }
1471
            else
1472
            {
1473
                this.errorMessage.removeClass('hidden');
1474
                helper.setMessage(this.errorMessage, message);
1475
            }
1476
            if (typeof this.replyStatus !== 'undefined') {
1477
                this.replyStatus.addClass('errorMessage');
1478
                this.replyStatus.addClass(this.errorMessage.attr('class'));
1479
                if (this.status.length)
1480
                {
1481
                    this.replyStatus.html(this.status.html());
1482
                }
1483
                else
1484
                {
1485
                    this.replyStatus.html(this.errorMessage.html());
1486
                }
1487
            }
1488
        },
1489
1490
        /**
1491
         * Display a status message
1492
         * (We use the same function for paste and reply to comments)
1493
         *
1494
         * @param string message : text to display
1495
         * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
1496
         */
1497
        showStatus: function(message, spin)
1498
        {
1499
            if (typeof this.replyStatus !== 'undefined') {
1500
                this.replyStatus.removeClass('errorMessage').text(message);
1501
            }
1502
            if (!message)
1503
            {
1504
                this.status.html(' ');
1505
                return;
1506
            }
1507
            if (message === '')
1508
            {
1509
                this.status.html(' ');
1510
                return;
1511
            }
1512
            this.status.removeClass('errorMessage').text(message);
1513
            if (spin)
1514
            {
1515
                var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />';
1516
                this.status.prepend(img);
1517
                if (typeof this.replyStatus !== 'undefined') {
1518
                    this.replyStatus.prepend(img);
1519
                }
1520
            }
1521
        },
1522
1523
        /**
1524
         * bind events to DOM elements
1525
         */
1526
        bindEvents: function()
1527
        {
1528
            this.burnAfterReading.change($.proxy(this.changeBurnAfterReading, this));
1529
            this.openDisc.change($.proxy(this.changeOpenDisc, this));
1530
            this.sendButton.click($.proxy(this.sendData, this));
1531
            this.cloneButton.click($.proxy(this.clonePaste, this));
1532
            this.rawTextButton.click($.proxy(this.rawText, this));
1533
            this.fileRemoveButton.click($.proxy(this.removeAttachment, this));
1534
            $('.reloadlink').click($.proxy(this.reloadPage, this));
1535
            this.message.keydown(this.supportTabs);
1536
            this.messageEdit.click($.proxy(this.viewEditor, this));
1537
            this.messagePreview.click($.proxy(this.viewPreview, this));
1538
1539
            // bootstrap template drop downs
1540
            $('ul.dropdown-menu li a', $('#expiration').parent()).click($.proxy(this.setExpiration, this));
1541
            $('ul.dropdown-menu li a', $('#formatter').parent()).click($.proxy(this.setFormat, this));
1542
            $('#language ul.dropdown-menu li a').click($.proxy(this.setLanguage, this));
1543
1544
            // page template drop down
1545
            $('#language select option').click($.proxy(this.setLanguage, this));
1546
1547
            // handle modal password request on decryption
1548
            this.passwordModal.on('shown.bs.modal', $.proxy(this.focusPasswordModal, this));
1549
            this.passwordModal.on('hidden.bs.modal', $.proxy(this.decryptPasswordModal, this));
1550
            this.passwordForm.submit($.proxy(this.submitPasswordModal, this));
1551
        },
1552
1553
        /**
1554
         * main application
1555
         */
1556
        init: function()
1557
        {
1558
            // hide "no javascript" message
1559
            $('#noscript').hide();
1560
1561
            // preload jQuery wrapped DOM elements and bind events
1562
            this.attach = $('#attach');
1563
            this.attachment = $('#attachment');
1564
            this.attachmentLink = $('#attachment a');
1565
            this.burnAfterReading = $('#burnafterreading');
1566
            this.burnAfterReadingOption = $('#burnafterreadingoption');
1567
            this.cipherData = $('#cipherdata');
1568
            this.clearText = $('#cleartext');
1569
            this.cloneButton = $('#clonebutton');
1570
            this.clonedFile = $('#clonedfile');
1571
            this.comments = $('#comments');
1572
            this.discussion = $('#discussion');
1573
            this.errorMessage = $('#errormessage');
1574
            this.expiration = $('#expiration');
1575
            this.fileRemoveButton = $('#fileremovebutton');
1576
            this.fileWrap = $('#filewrap');
1577
            this.formatter = $('#formatter');
1578
            this.image = $('#image');
1579
            this.message = $('#message');
1580
            this.messageEdit = $('#messageedit');
1581
            this.messagePreview = $('#messagepreview');
1582
            this.newButton = $('#newbutton');
1583
            this.openDisc = $('#opendisc');
1584
            this.openDiscussion = $('#opendiscussion');
1585
            this.password = $('#password');
1586
            this.passwordInput = $('#passwordinput');
1587
            this.passwordModal = $('#passwordmodal');
1588
            this.passwordForm = $('#passwordform');
1589
            this.passwordDecrypt = $('#passworddecrypt');
1590
            this.pasteResult = $('#pasteresult');
1591
            this.prettyMessage = $('#prettymessage');
1592
            this.prettyPrint = $('#prettyprint');
1593
            this.preview = $('#preview');
1594
            this.rawTextButton = $('#rawtextbutton');
1595
            this.remainingTime = $('#remainingtime');
1596
            this.sendButton = $('#sendbutton');
1597
            this.status = $('#status');
1598
            this.bindEvents();
1599
1600
            // Display status returned by php code if any (eg. Paste was properly deleted.)
1601
            if (this.status.text().length > 0)
1602
            {
1603
                this.showStatus(this.status.text(), false);
1604
                return;
1605
            }
1606
1607
            // Keep line height even if content empty.
1608
            this.status.html(' ');
1609
1610
            // Display an existing paste
1611
            if (this.cipherData.text().length > 1)
1612
            {
1613
                // Missing decryption key in URL?
1614
                if (window.location.hash.length === 0)
1615
                {
1616
                    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?)'));
1617
                    return;
1618
                }
1619
1620
                // Show proper elements on screen.
1621
                this.stateExistingPaste();
1622
                this.displayMessages();
1623
            }
1624
            // Display error message from php code.
1625
            else if (this.errorMessage.text().length > 1)
1626
            {
1627
                this.showError(this.errorMessage.text());
1628
            }
1629
            // Create a new paste.
1630
            else
1631
            {
1632
                this.newPaste();
1633
            }
1634
        }
1635
    }
1636
1637
    /**
1638
     * main application start, called when DOM is fully loaded
1639
     * runs privatebin when translations were loaded
1640
     */
1641
    i18n.loadTranslations($.proxy(privatebin.init, privatebin));
1642
});
1643