Completed
Push — master ( d76ccc...20fea8 )
by rugk
08:07 queued 04:48
created

privatebin.js ➔ ... ➔ privatebin.setFormat   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 11
rs 9.4285
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.1
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)
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
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);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

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