Completed
Push — master ( 065f5e...339ab5 )
by El
03:20
created

controller.displayMessages   F

Complexity

Conditions 23
Paths 2518

Size

Total Lines 173

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
nc 2518
nop 1
dl 0
loc 173
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like controller.displayMessages often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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