Completed
Push — master ( 151005...5f0926 )
by El
03:15
created

privatebin.js ➔ ... ➔ format.replace   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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