2012-04-22 18:45:45 +08:00
/ * *
2016-07-11 17:58:15 +08:00
* PrivateBin
2012-04-23 22:30:02 +08:00
*
2012-05-01 04:58:08 +08:00
* a zero - knowledge paste bin
*
2017-01-14 22:29:12 +08:00
* @ see { @ link https : //github.com/PrivateBin/PrivateBin}
* @ copyright 2012 Sébastien SAUVAGE ( { @ link http : //sebsauvage.net})
* @ license { @ link https : //www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
2021-04-05 22:44:12 +08:00
* @ version 1.3 . 5
2017-01-14 22:29:12 +08:00
* @ name PrivateBin
* @ namespace
2012-04-22 18:45:45 +08:00
* /
2012-04-22 03:59:45 +08:00
2019-09-23 03:18:19 +08:00
// global Base64, DOMPurify, FileReader, RawDeflate, history, navigator, prettyPrint, prettyPrintOne, showdown, kjua
2019-08-16 07:28:42 +08:00
jQuery . fn . draghover = function ( ) {
2019-09-14 15:41:52 +08:00
'use strict' ;
2019-08-16 07:28:42 +08:00
return this . each ( function ( ) {
let collection = $ ( ) ,
self = $ ( this ) ;
2020-04-23 17:25:24 +08:00
2019-08-16 07:28:42 +08:00
self . on ( 'dragenter' , function ( e ) {
if ( collection . length === 0 ) {
self . trigger ( 'draghoverstart' ) ;
}
collection = collection . add ( e . target ) ;
} ) ;
2020-04-23 17:25:24 +08:00
2019-08-16 07:28:42 +08:00
self . on ( 'dragleave drop' , function ( e ) {
collection = collection . not ( e . target ) ;
if ( collection . length === 0 ) {
self . trigger ( 'draghoverend' ) ;
}
} ) ;
} ) ;
} ;
2017-02-15 05:21:55 +08:00
// main application start, called when DOM is fully loaded
jQuery ( document ) . ready ( function ( ) {
2018-02-22 05:51:31 +08:00
'use strict' ;
2017-02-15 05:21:55 +08:00
// run main controller
$ . PrivateBin . Controller . init ( ) ;
} ) ;
2018-10-21 01:53:21 +08:00
jQuery . PrivateBin = ( function ( $ , RawDeflate ) {
2017-02-13 01:08:08 +08:00
'use strict' ;
2018-12-28 04:32:13 +08:00
/ * *
* zlib library interface
*
* @ private
* /
let z ;
2019-05-25 19:20:39 +08:00
/ * *
* CryptoData class
*
* bundles helper fuctions used in both paste and comment formats
*
* @ name CryptoData
* @ class
* /
function CryptoData ( data ) {
this . v = 1 ;
// store all keys in the default locations for drop-in replacement
for ( let key in data ) {
this [ key ] = data [ key ] ;
}
/ * *
* gets the cipher data ( cipher text + adata )
*
* @ name Paste . getCipherData
* @ function
* @ return { Array } | { string }
* /
this . getCipherData = function ( )
{
return this . v === 1 ? this . data : [ this . ct , this . adata ] ;
}
}
2019-05-25 16:10:59 +08:00
/ * *
* Paste class
2019-05-25 19:20:39 +08:00
*
2019-05-25 16:10:59 +08:00
* bundles helper fuctions around the paste formats
*
* @ name Paste
* @ class
* /
function Paste ( data ) {
2019-05-25 19:20:39 +08:00
// inherit constructor and methods of CryptoData
CryptoData . call ( this , data ) ;
/ * *
* gets the used formatter
*
* @ name Paste . getFormat
* @ function
* @ return { string }
* /
this . getFormat = function ( )
{
return this . v === 1 ? this . meta . formatter : this . adata [ 1 ] ;
}
/ * *
* gets the remaining seconds before the paste expires
*
* returns 0 if there is no expiration
*
* @ name Paste . getTimeToLive
* @ function
* @ return { string }
* /
this . getTimeToLive = function ( )
{
return ( this . v === 1 ? this . meta . remaining _time : this . meta . time _to _live ) || 0 ;
}
/ * *
* is burn - after - reading enabled
*
* @ name Paste . isBurnAfterReadingEnabled
* @ function
* @ return { bool }
* /
this . isBurnAfterReadingEnabled = function ( )
{
return ( this . v === 1 ? this . meta . burnafterreading : this . adata [ 3 ] ) ;
}
/ * *
* are discussions enabled
*
* @ name Paste . isDiscussionEnabled
* @ function
* @ return { bool }
* /
this . isDiscussionEnabled = function ( )
{
return ( this . v === 1 ? this . meta . opendiscussion : this . adata [ 2 ] ) ;
}
}
/ * *
* Comment class
*
* bundles helper fuctions around the comment formats
*
* @ name Comment
* @ class
* /
function Comment ( data ) {
// inherit constructor and methods of CryptoData
CryptoData . call ( this , data ) ;
/ * *
* gets the UNIX timestamp of the comment creation
*
* @ name Paste . getCreated
* @ function
* @ return { int }
* /
this . getCreated = function ( )
{
return this . meta [ this . v === 1 ? 'postdate' : 'created' ] ;
}
/ * *
* gets the icon of the comment submitter
*
* @ name Paste . getIcon
* @ function
* @ return { string }
* /
this . getIcon = function ( )
{
return this . meta [ this . v === 1 ? 'vizhash' : 'icon' ] || '' ;
2019-05-25 16:10:59 +08:00
}
}
2015-09-05 23:12:11 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* static Helper methods
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Helper
2017-01-14 22:29:12 +08:00
* @ class
2015-09-05 23:12:11 +08:00
* /
2018-12-30 01:40:59 +08:00
const Helper = ( function ( ) {
const me = { } ;
2017-02-09 03:12:22 +08:00
2020-01-18 14:30:01 +08:00
/ * *
* character to HTML entity lookup table
*
* @ see { @ link https : //github.com/janl/mustache.js/blob/master/mustache.js#L60}
* @ name Helper . entityMap
* @ private
* @ enum { Object }
* @ readonly
* /
2020-02-02 14:08:38 +08:00
const entityMap = {
2020-01-18 14:30:01 +08:00
'&' : '&' ,
'<' : '<' ,
'>' : '>' ,
'"' : '"' ,
"'" : ''' ,
'/' : '/' ,
'`' : '`' ,
'=' : '='
} ;
2020-03-01 15:54:48 +08:00
/ * *
* number of seconds in a minute
*
* @ name Helper . minute
* @ private
* @ enum { number }
* @ readonly
* /
const minute = 60 ;
/ * *
* number of seconds in an hour
*
* = 60 * 60 seconds
*
* @ name Helper . minute
* @ private
* @ enum { number }
* @ readonly
* /
const hour = 3600 ;
/ * *
* number of seconds in a day
*
* = 60 * 60 * 24 seconds
*
* @ name Helper . day
* @ private
* @ enum { number }
* @ readonly
* /
const day = 86400 ;
2020-05-30 17:55:41 +08:00
/ * *
* number of seconds in a week
*
* = 60 * 60 * 24 * 7 seconds
*
* @ name Helper . week
* @ private
* @ enum { number }
* @ readonly
* /
const week = 604800 ;
2020-03-01 15:54:48 +08:00
/ * *
* number of seconds in a month ( 30 days , an approximation )
*
* = 60 * 60 * 24 * 30 seconds
*
* @ name Helper . month
* @ private
* @ enum { number }
* @ readonly
* /
const month = 2592000 ;
/ * *
* number of seconds in a non - leap year
*
* = 60 * 60 * 24 * 365 seconds
*
* @ name Helper . year
* @ private
* @ enum { number }
* @ readonly
* /
const year = 31536000 ;
2017-02-09 03:12:22 +08:00
/ * *
* cache for script location
*
2017-03-14 03:24:18 +08:00
* @ name Helper . baseUri
2017-02-09 03:12:22 +08:00
* @ private
* @ enum { string | null }
* /
2018-12-30 01:40:59 +08:00
let baseUri = null ;
2017-02-09 03:12:22 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-01-14 22:29:12 +08:00
* converts a duration ( in seconds ) into human friendly approximation
2015-09-05 23:12:11 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Helper . secondsToHuman
2017-01-14 22:29:12 +08:00
* @ function
* @ param { number } seconds
* @ return { Array }
2015-09-05 23:12:11 +08:00
* /
2017-02-09 03:12:22 +08:00
me . secondsToHuman = function ( seconds )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
let v ;
2020-03-01 15:54:48 +08:00
if ( seconds < minute )
2015-09-05 23:12:11 +08:00
{
2016-07-11 21:47:42 +08:00
v = Math . floor ( seconds ) ;
2015-09-06 21:54:43 +08:00
return [ v , 'second' ] ;
2015-09-05 23:12:11 +08:00
}
2020-03-01 15:54:48 +08:00
if ( seconds < hour )
2015-09-05 23:12:11 +08:00
{
2020-03-01 15:54:48 +08:00
v = Math . floor ( seconds / minute ) ;
2015-09-06 21:54:43 +08:00
return [ v , 'minute' ] ;
2015-09-05 23:12:11 +08:00
}
2020-03-01 15:54:48 +08:00
if ( seconds < day )
2015-09-05 23:12:11 +08:00
{
2020-03-01 15:54:48 +08:00
v = Math . floor ( seconds / hour ) ;
2015-09-06 21:54:43 +08:00
return [ v , 'hour' ] ;
2015-09-05 23:12:11 +08:00
}
// If less than 2 months, display in days:
2020-03-01 15:54:48 +08:00
if ( seconds < ( 2 * month ) )
2015-09-05 23:12:11 +08:00
{
2020-03-01 15:54:48 +08:00
v = Math . floor ( seconds / day ) ;
2015-09-06 21:54:43 +08:00
return [ v , 'day' ] ;
2015-09-05 23:12:11 +08:00
}
2020-03-01 15:54:48 +08:00
v = Math . floor ( seconds / month ) ;
2015-09-06 21:54:43 +08:00
return [ v , 'month' ] ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2020-03-01 15:54:48 +08:00
/ * *
* converts a duration string into seconds
*
* The string is expected to be optional digits , followed by a time .
* Supported times are : min , hour , day , month , year , never
* Examples : 5 min , 13 hour , never
*
* @ name Helper . durationToSeconds
* @ function
* @ param { String } duration
* @ return { number }
* /
me . durationToSeconds = function ( duration )
{
2020-05-30 17:55:41 +08:00
let pieces = duration . split ( /(\D+)/ ) ,
2020-03-01 15:54:48 +08:00
factor = pieces [ 0 ] || 0 ,
timespan = pieces [ 1 ] || pieces [ 0 ] ;
switch ( timespan )
{
case 'min' :
return factor * minute ;
case 'hour' :
return factor * hour ;
case 'day' :
return factor * day ;
2020-05-30 17:55:41 +08:00
case 'week' :
return factor * week ;
2020-03-01 15:54:48 +08:00
case 'month' :
return factor * month ;
case 'year' :
return factor * year ;
case 'never' :
return 0 ;
default :
return factor ;
}
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-01-14 22:29:12 +08:00
* text range selection
2015-09-05 23:12:11 +08:00
*
2017-01-14 22:29:12 +08:00
* @ see { @ link https : //stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
2017-02-15 05:21:55 +08:00
* @ name Helper . selectText
2017-01-14 22:29:12 +08:00
* @ function
2017-02-09 03:12:22 +08:00
* @ param { HTMLElement } element
2015-09-05 23:12:11 +08:00
* /
2017-02-09 03:12:22 +08:00
me . selectText = function ( element )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
let range , selection ;
2015-09-05 23:12:11 +08:00
// MS
2017-02-15 05:21:55 +08:00
if ( document . body . createTextRange ) {
2017-02-09 03:12:22 +08:00
range = document . body . createTextRange ( ) ;
range . moveToElementText ( element ) ;
2015-09-05 23:12:11 +08:00
range . select ( ) ;
2017-11-14 04:57:49 +08:00
} else if ( window . getSelection ) {
2015-09-05 23:12:11 +08:00
selection = window . getSelection ( ) ;
2017-02-09 03:12:22 +08:00
range = document . createRange ( ) ;
range . selectNodeContents ( element ) ;
2015-09-05 23:12:11 +08:00
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
}
2018-01-06 16:26:10 +08:00
} ;
2015-09-12 23:33:16 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2020-03-07 05:18:38 +08:00
* convert URLs to clickable links in the provided element .
2019-05-25 19:20:39 +08:00
*
2015-09-05 23:12:11 +08:00
* URLs to handle :
2017-01-14 22:29:12 +08:00
* < pre >
2015-09-05 23:12:11 +08:00
* magnet : ? xt . 1 = urn : sha1 : YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C & xt . 2 = urn : sha1 : TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
2017-04-11 22:34:13 +08:00
* https : //example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
2017-01-14 22:29:12 +08:00
* http : //user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
* < / p r e >
2015-09-05 23:12:11 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Helper . urls2links
2017-01-14 22:29:12 +08:00
* @ function
2020-03-07 05:18:38 +08:00
* @ param { HTMLElement } element
2015-09-05 23:12:11 +08:00
* /
2020-03-07 05:18:38 +08:00
me . urls2links = function ( element )
2015-09-05 23:12:11 +08:00
{
2020-03-07 05:18:38 +08:00
element . html (
2020-05-30 18:05:20 +08:00
DOMPurify . sanitize (
element . html ( ) . replace (
/(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig ,
2020-06-02 20:50:14 +08:00
'<a href="$1" rel="nofollow noopener noreferrer">$1</a>'
2020-05-30 18:05:20 +08:00
)
2020-03-07 05:18:38 +08:00
)
2020-03-07 03:57:15 +08:00
) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-06 19:07:46 +08:00
/ * *
* minimal sprintf emulation for % s and % d formats
*
2017-02-12 02:34:51 +08:00
* Note that this function needs the parameters in the same order as the
* format strings appear in the string , contrary to the original .
*
2017-01-14 22:29:12 +08:00
* @ see { @ link https : //stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
2017-02-15 05:21:55 +08:00
* @ name Helper . sprintf
2017-01-14 22:29:12 +08:00
* @ function
* @ param { string } format
* @ param { ... * } args - one or multiple parameters injected into format string
* @ return { string }
2015-09-06 19:07:46 +08:00
* /
2017-02-09 03:12:22 +08:00
me . sprintf = function ( )
2015-09-06 19:07:46 +08:00
{
2018-12-30 01:40:59 +08:00
const args = Array . prototype . slice . call ( arguments ) ;
let format = args [ 0 ] ,
2015-09-06 19:07:46 +08:00
i = 1 ;
2017-02-12 02:34:51 +08:00
return format . replace ( /%(s|d)/g , function ( m ) {
2018-12-30 01:40:59 +08:00
let val = args [ i ] ;
2020-01-25 16:16:14 +08:00
if ( m === '%d' ) {
val = parseFloat ( val ) ;
if ( isNaN ( val ) ) {
val = 0 ;
}
2015-09-06 19:07:46 +08:00
}
2017-02-12 02:34:51 +08:00
++ i ;
2015-09-06 19:07:46 +08:00
return val ;
} ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-22 04:43:00 +08:00
2015-09-19 17:21:13 +08:00
/ * *
* get value of cookie , if it was set , empty string otherwise
*
2017-01-14 22:29:12 +08:00
* @ see { @ link http : //www.w3schools.com/js/js_cookies.asp}
2017-02-15 05:21:55 +08:00
* @ name Helper . getCookie
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 00:11:21 +08:00
* @ param { string } cname - may not be empty
2017-01-14 22:29:12 +08:00
* @ return { string }
2015-09-19 17:21:13 +08:00
* /
2017-02-09 03:12:22 +08:00
me . getCookie = function ( cname ) {
2018-12-30 01:40:59 +08:00
const name = cname + '=' ,
ca = document . cookie . split ( ';' ) ;
for ( let i = 0 ; i < ca . length ; ++ i ) {
let c = ca [ i ] ;
2017-02-05 21:47:03 +08:00
while ( c . charAt ( 0 ) === ' ' )
{
c = c . substring ( 1 ) ;
}
2016-07-11 21:47:42 +08:00
if ( c . indexOf ( name ) === 0 )
{
return c . substring ( name . length , c . length ) ;
}
2015-09-19 17:21:13 +08:00
}
return '' ;
2018-01-06 16:26:10 +08:00
} ;
2016-07-19 22:12:11 +08:00
2017-02-05 21:47:03 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* get the current location ( without search or hash part of the URL ) ,
2017-04-11 22:34:13 +08:00
* eg . https : //example.com/path/?aaaa#bbbb --> https://example.com/path/
2017-02-05 21:47:03 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Helper . baseUri
2017-02-05 21:47:03 +08:00
* @ function
2017-02-15 05:21:55 +08:00
* @ return { string }
2017-02-05 21:47:03 +08:00
* /
2017-02-15 05:21:55 +08:00
me . baseUri = function ( )
2017-02-05 21:47:03 +08:00
{
2017-02-09 03:12:22 +08:00
// check for cached version
2017-02-15 05:21:55 +08:00
if ( baseUri !== null ) {
return baseUri ;
2017-02-05 21:47:03 +08:00
}
2018-08-04 23:25:59 +08:00
baseUri = window . location . origin + window . location . pathname ;
2017-02-15 05:21:55 +08:00
return baseUri ;
2018-01-06 16:26:10 +08:00
} ;
2016-07-19 22:12:11 +08:00
2019-05-25 19:20:39 +08:00
/ * *
* wrap an object into a Paste , used for mocking in the unit tests
*
* @ name Helper . PasteFactory
* @ function
* @ param { object } data
* @ return { Paste }
* /
me . PasteFactory = function ( data )
{
return new Paste ( data ) ;
} ;
/ * *
* wrap an object into a Comment , used for mocking in the unit tests
*
* @ name Helper . CommentFactory
* @ function
* @ param { object } data
* @ return { Comment }
* /
me . CommentFactory = function ( data )
{
return new Comment ( data ) ;
} ;
2020-01-18 14:30:01 +08:00
/ * *
* convert all applicable characters to HTML entities
*
2020-01-18 14:36:43 +08:00
* @ see { @ link https : //cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html}
2020-01-18 14:30:01 +08:00
* @ name Helper . htmlEntities
* @ function
* @ param { string } str
* @ return { string } escaped HTML
* /
me . htmlEntities = function ( str ) {
return String ( str ) . replace (
/[&<>"'`=\/]/g , function ( s ) {
return entityMap [ s ] ;
2020-01-18 14:36:43 +08:00
}
) ;
2020-01-18 14:30:01 +08:00
}
2019-08-22 05:36:22 +08:00
/ * *
* calculate expiration date given initial date and expiration period
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name Helper . calculateExpirationDate
* @ function
* @ param { Date } initialDate - may not be empty
* @ param { string | number } expirationDisplayStringOrSecondsToExpire - may not be empty
* @ return { Date }
* /
me . calculateExpirationDate = function ( initialDate , expirationDisplayStringOrSecondsToExpire ) {
2020-03-01 15:54:48 +08:00
let expirationDate = new Date ( initialDate ) ,
secondsToExpiration = expirationDisplayStringOrSecondsToExpire ;
2019-08-22 05:36:22 +08:00
if ( typeof expirationDisplayStringOrSecondsToExpire === 'string' ) {
2020-03-01 15:54:48 +08:00
secondsToExpiration = me . durationToSeconds ( expirationDisplayStringOrSecondsToExpire ) ;
2019-08-22 05:36:22 +08:00
}
2020-04-23 17:25:24 +08:00
2019-08-22 05:36:22 +08:00
if ( typeof secondsToExpiration !== 'number' ) {
throw new Error ( 'Cannot calculate expiration date.' ) ;
}
if ( secondsToExpiration === 0 ) {
return null ;
}
expirationDate = expirationDate . setUTCSeconds ( expirationDate . getUTCSeconds ( ) + secondsToExpiration ) ;
return expirationDate ;
2020-01-04 18:34:16 +08:00
} ;
2020-02-02 14:08:38 +08:00
/ * *
* resets state , used for unit testing
*
* @ name Helper . reset
* @ function
* /
me . reset = function ( )
{
baseUri = null ;
} ;
2017-02-09 03:12:22 +08:00
return me ;
2017-03-05 19:11:55 +08:00
} ) ( ) ;
2015-09-05 23:12:11 +08:00
2015-09-06 19:07:46 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* internationalization module
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name I18n
2017-01-14 22:29:12 +08:00
* @ class
2015-09-06 19:07:46 +08:00
* /
2018-12-30 01:40:59 +08:00
const I18n = ( function ( ) {
const me = { } ;
2017-02-09 03:12:22 +08:00
2017-02-15 05:21:55 +08:00
/ * *
* const for string of loaded language
*
2017-03-14 03:24:18 +08:00
* @ name I18n . languageLoadedEvent
2017-02-15 05:21:55 +08:00
* @ private
* @ prop { string }
* @ readonly
* /
2018-12-30 01:40:59 +08:00
const languageLoadedEvent = 'languageLoaded' ;
2017-02-15 05:21:55 +08:00
2015-09-06 21:54:43 +08:00
/ * *
* supported languages , minus the built in 'en'
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name I18n . supportedLanguages
2017-02-09 03:12:22 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ prop { string [ ] }
* @ readonly
2015-09-06 21:54:43 +08:00
* /
2021-04-17 00:27:12 +08:00
const supportedLanguages = [ 'bg' , 'ca' , 'cs' , 'de' , 'es' , 'et' , 'fr' , 'he' , 'hu' , 'id' , 'it' , 'lt' , 'no' , 'nl' , 'pl' , 'pt' , 'oc' , 'ru' , 'sl' , 'uk' , 'zh' ] ;
2017-02-09 03:12:22 +08:00
/ * *
* built in language
*
2017-03-14 03:24:18 +08:00
* @ name I18n . language
2017-02-09 03:12:22 +08:00
* @ private
2017-02-15 05:21:55 +08:00
* @ prop { string | null }
2017-02-09 03:12:22 +08:00
* /
2018-12-30 01:40:59 +08:00
let language = null ;
2017-02-09 03:12:22 +08:00
/ * *
* translation cache
*
2017-03-14 03:24:18 +08:00
* @ name I18n . translations
2017-02-09 03:12:22 +08:00
* @ private
* @ enum { Object }
* /
2018-12-30 01:40:59 +08:00
let translations = { } ;
2015-09-06 21:54:43 +08:00
2015-09-06 19:07:46 +08:00
/ * *
2017-03-14 03:24:18 +08:00
* translate a string , alias for I18n . translate
2015-09-06 19:07:46 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name I18n . _
2017-01-14 22:29:12 +08:00
* @ function
2017-02-15 05:21:55 +08:00
* @ param { jQuery } $element - optional
2017-01-14 22:29:12 +08:00
* @ param { string } messageId
* @ param { ... * } args - one or multiple parameters injected into placeholders
* @ return { string }
2015-09-06 19:07:46 +08:00
* /
2017-02-09 03:12:22 +08:00
me . _ = function ( )
2015-09-06 19:07:46 +08:00
{
2017-02-15 05:21:55 +08:00
return me . translate . apply ( this , arguments ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-06 19:07:46 +08:00
/ * *
* translate a string
*
2020-01-25 16:07:29 +08:00
* Optionally pass a jQuery element as the first parameter , to automatically
* let the text of this element be replaced . In case the ( asynchronously
2020-01-25 16:16:14 +08:00
* loaded ) language is not downloaded yet , this will make sure the string
* is replaced when it eventually gets loaded . Using this is both simpler
* and more secure , as it avoids potential XSS when inserting text .
* The next parameter is the message ID , matching the ones found in
* the translation files under the i18n directory .
* Any additional parameters will get inserted into the message ID in
* place of % s ( strings ) or % d ( digits ) , applying the appropriate plural
* in case of digits . See also Helper . sprintf ( ) .
2017-02-15 05:21:55 +08:00
*
* @ name I18n . translate
2017-01-14 22:29:12 +08:00
* @ function
2020-01-25 16:07:29 +08:00
* @ param { jQuery } $element - optional
2017-01-14 22:29:12 +08:00
* @ param { string } messageId
* @ param { ... * } args - one or multiple parameters injected into placeholders
2020-01-25 16:07:29 +08:00
* @ return { string }
2015-09-06 19:07:46 +08:00
* /
2017-02-09 03:12:22 +08:00
me . translate = function ( )
2015-09-06 19:07:46 +08:00
{
2017-02-15 05:21:55 +08:00
// convert parameters to array
2018-12-30 01:40:59 +08:00
let args = Array . prototype . slice . call ( arguments ) ,
2017-02-15 05:21:55 +08:00
messageId ,
$element = null ;
// parse arguments
if ( args [ 0 ] instanceof jQuery ) {
// optional jQuery element as first parameter
$element = args [ 0 ] ;
args . shift ( ) ;
2016-07-11 21:47:42 +08:00
}
2017-02-15 05:21:55 +08:00
// extract messageId from arguments
2018-12-30 01:40:59 +08:00
let usesPlurals = $ . isArray ( args [ 0 ] ) ;
2017-02-15 05:21:55 +08:00
if ( usesPlurals ) {
2015-09-06 21:54:43 +08:00
// use the first plural form as messageId, otherwise the singular
2018-02-22 05:51:31 +08:00
messageId = args [ 0 ] . length > 1 ? args [ 0 ] [ 1 ] : args [ 0 ] [ 0 ] ;
2017-02-15 05:21:55 +08:00
} else {
2015-09-06 21:54:43 +08:00
messageId = args [ 0 ] ;
}
2017-02-15 05:21:55 +08:00
if ( messageId . length === 0 ) {
2016-07-11 21:47:42 +08:00
return messageId ;
}
2017-02-15 05:21:55 +08:00
// if no translation string cannot be found (in translations object)
2017-03-13 00:08:12 +08:00
if ( ! translations . hasOwnProperty ( messageId ) || language === null ) {
2017-02-15 05:21:55 +08:00
// if language is still loading and we have an elemt assigned
if ( language === null && $element !== null ) {
// handle the error by attaching the language loaded event
2018-12-30 01:40:59 +08:00
let orgArguments = arguments ;
2017-02-15 05:21:55 +08:00
$ ( document ) . on ( languageLoadedEvent , function ( ) {
// re-execute this function
me . translate . apply ( this , orgArguments ) ;
} ) ;
// and fall back to English for now until the real language
// file is loaded
}
2017-04-12 04:21:30 +08:00
// for all other languages than English for which this behaviour
2017-02-15 05:21:55 +08:00
// is expected as it is built-in, log error
2017-03-13 00:08:12 +08:00
if ( language !== null && language !== 'en' ) {
2017-02-15 05:21:55 +08:00
console . error ( 'Missing translation for: \'' + messageId + '\' in language ' + language ) ;
// fallback to English
2016-07-11 21:47:42 +08:00
}
2017-02-15 05:21:55 +08:00
// save English translation (should be the same on both sides)
2017-02-09 03:12:22 +08:00
translations [ messageId ] = args [ 0 ] ;
2015-09-06 21:54:43 +08:00
}
2017-02-15 05:21:55 +08:00
// lookup plural translation
if ( usesPlurals && $ . isArray ( translations [ messageId ] ) ) {
2018-12-30 01:40:59 +08:00
let n = parseInt ( args [ 1 ] || 1 , 10 ) ,
2017-02-09 03:12:22 +08:00
key = me . getPluralForm ( n ) ,
maxKey = translations [ messageId ] . length - 1 ;
2017-02-15 05:21:55 +08:00
if ( key > maxKey ) {
2016-07-11 21:47:42 +08:00
key = maxKey ;
}
2017-02-09 03:12:22 +08:00
args [ 0 ] = translations [ messageId ] [ key ] ;
2015-09-06 21:54:43 +08:00
args [ 1 ] = n ;
2017-02-15 05:21:55 +08:00
} else {
// lookup singular translation
2017-02-09 03:12:22 +08:00
args [ 0 ] = translations [ messageId ] ;
2015-09-06 21:54:43 +08:00
}
2017-02-15 05:21:55 +08:00
2020-01-18 14:20:05 +08:00
// messageID may contain links, but should be from a trusted source (code or translation JSON files)
2020-01-14 02:17:30 +08:00
let containsLinks = args [ 0 ] . indexOf ( '<a' ) !== - 1 ;
2020-01-18 17:44:35 +08:00
// prevent double encoding, when we insert into a text node
2020-02-01 05:42:42 +08:00
if ( containsLinks || $element === null ) {
2020-01-18 17:44:35 +08:00
for ( let i = 0 ; i < args . length ; ++ i ) {
// parameters (i > 0) may never contain HTML as they may come from untrusted parties
2020-02-02 14:08:38 +08:00
if ( ( containsLinks ? i > 1 : i > 0 ) || ! containsLinks ) {
2020-01-18 17:44:35 +08:00
args [ i ] = Helper . htmlEntities ( args [ i ] ) ;
}
2020-01-18 14:20:05 +08:00
}
}
2017-02-15 05:21:55 +08:00
// format string
2018-12-30 01:40:59 +08:00
let output = Helper . sprintf . apply ( this , args ) ;
2017-02-15 05:21:55 +08:00
2020-01-18 17:44:35 +08:00
if ( containsLinks ) {
// only allow tags/attributes we actually use in translations
output = DOMPurify . sanitize (
output , {
2020-02-01 15:46:59 +08:00
ALLOWED _TAGS : [ 'a' , 'i' , 'span' ] ,
2020-01-18 17:44:35 +08:00
ALLOWED _ATTR : [ 'href' , 'id' ]
}
) ;
}
2020-01-25 16:07:29 +08:00
// if $element is given, insert translation
if ( $element !== null ) {
if ( containsLinks ) {
$element . html ( output ) ;
} else {
// text node takes care of entity encoding
2020-02-01 05:42:42 +08:00
$element . text ( output ) ;
2020-01-25 16:07:29 +08:00
}
return '' ;
2015-09-06 19:07:46 +08:00
}
2017-02-15 05:21:55 +08:00
2020-01-25 16:07:29 +08:00
return output ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-06 19:07:46 +08:00
2015-09-09 02:48:18 +08:00
/ * *
* per language functions to use to determine the plural form
*
2021-04-17 00:27:12 +08:00
* @ see { @ link https : //localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
2017-02-15 05:21:55 +08:00
* @ name I18n . getPluralForm
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { int } n
* @ return { int } array key
2015-09-09 02:48:18 +08:00
* /
2017-02-09 03:12:22 +08:00
me . getPluralForm = function ( n ) {
switch ( language )
2015-09-09 02:48:18 +08:00
{
2019-06-23 18:06:36 +08:00
case 'cs' :
return n === 1 ? 0 : ( n >= 2 && n <= 4 ? 1 : 2 ) ;
2015-09-09 02:48:18 +08:00
case 'fr' :
2017-01-08 14:56:56 +08:00
case 'oc' :
2016-04-27 02:21:30 +08:00
case 'zh' :
2018-02-22 05:51:31 +08:00
return n > 1 ? 1 : 0 ;
2021-01-08 04:16:03 +08:00
case 'he' :
return n === 1 ? 0 : ( n === 2 ? 1 : ( ( n < 0 || n > 10 ) && ( n % 10 === 0 ) ? 2 : 3 ) ) ;
2021-03-09 12:54:06 +08:00
case 'id' :
return 0 ;
2021-01-08 04:16:03 +08:00
case 'lt' :
return n % 10 === 1 && n % 100 !== 11 ? 0 : ( ( n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ) ;
2015-09-09 02:48:18 +08:00
case 'pl' :
2018-02-22 05:51:31 +08:00
return n === 1 ? 0 : ( n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ) ;
2016-12-16 17:21:15 +08:00
case 'ru' :
2019-10-18 17:31:40 +08:00
case 'uk' :
return n % 10 === 1 && n % 100 !== 11 ? 0 : ( n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ) ;
2017-01-01 21:35:39 +08:00
case 'sl' :
2018-02-22 05:51:31 +08:00
return n % 100 === 1 ? 1 : ( n % 100 === 2 ? 2 : ( n % 100 === 3 || n % 100 === 4 ? 3 : 0 ) ) ;
2021-04-17 00:27:12 +08:00
// bg, ca, de, en, es, et, hu, it, nl, no, pt
2015-09-09 02:48:18 +08:00
default :
2018-02-22 05:51:31 +08:00
return n !== 1 ? 1 : 0 ;
2015-09-09 02:48:18 +08:00
}
2018-01-06 16:26:10 +08:00
} ;
2015-09-09 02:48:18 +08:00
2015-09-06 21:54:43 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* load translations into cache
2015-09-06 21:54:43 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name I18n . loadTranslations
2017-01-14 22:29:12 +08:00
* @ function
2015-09-06 21:54:43 +08:00
* /
2017-02-09 03:12:22 +08:00
me . loadTranslations = function ( )
2015-09-06 19:07:46 +08:00
{
2018-12-30 01:40:59 +08:00
let newLanguage = Helper . getCookie ( 'lang' ) ;
2017-02-08 20:20:51 +08:00
2017-02-09 03:12:22 +08:00
// auto-select language based on browser settings
2017-02-15 05:21:55 +08:00
if ( newLanguage . length === 0 ) {
2018-04-07 10:53:00 +08:00
newLanguage = ( navigator . language || navigator . userLanguage || 'en' ) . substring ( 0 , 2 ) ;
2017-02-05 21:47:03 +08:00
}
2015-09-06 19:07:46 +08:00
2017-02-15 05:21:55 +08:00
// if language is already used skip update
2017-02-13 01:08:08 +08:00
if ( newLanguage === language ) {
2017-02-09 03:12:22 +08:00
return ;
2015-09-09 02:48:18 +08:00
}
2015-09-06 21:54:43 +08:00
2017-02-15 05:21:55 +08:00
// if language is built-in (English) skip update
if ( newLanguage === 'en' ) {
language = 'en' ;
return ;
2015-09-09 02:48:18 +08:00
}
2015-09-06 19:07:46 +08:00
2017-02-09 03:12:22 +08:00
// if language is not supported, show error
2017-02-13 01:08:08 +08:00
if ( supportedLanguages . indexOf ( newLanguage ) === - 1 ) {
2017-02-09 03:12:22 +08:00
console . error ( 'Language \'%s\' is not supported. Translation failed, fallback to English.' , newLanguage ) ;
2017-02-15 05:21:55 +08:00
language = 'en' ;
return ;
2017-02-09 03:12:22 +08:00
}
2015-09-06 21:54:43 +08:00
2017-02-15 05:21:55 +08:00
// load strings from JSON
2017-02-09 03:12:22 +08:00
$ . getJSON ( 'i18n/' + newLanguage + '.json' , function ( data ) {
language = newLanguage ;
translations = data ;
2017-02-15 05:21:55 +08:00
$ ( document ) . triggerHandler ( languageLoadedEvent ) ;
2017-02-09 03:12:22 +08:00
} ) . fail ( function ( data , textStatus , errorMsg ) {
console . error ( 'Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.' , newLanguage , textStatus , errorMsg ) ;
2017-02-15 05:21:55 +08:00
language = 'en' ;
2017-02-09 03:12:22 +08:00
} ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-06 19:07:46 +08:00
2017-03-25 16:41:24 +08:00
/ * *
* resets state , used for unit testing
*
* @ name I18n . reset
* @ function
* /
2017-03-26 15:24:42 +08:00
me . reset = function ( mockLanguage , mockTranslations )
2017-03-25 16:41:24 +08:00
{
2017-03-26 15:24:42 +08:00
language = mockLanguage || null ;
translations = mockTranslations || { } ;
2018-01-06 16:26:10 +08:00
} ;
2017-03-25 16:41:24 +08:00
2017-02-09 03:12:22 +08:00
return me ;
2017-03-26 15:24:42 +08:00
} ) ( ) ;
2015-09-06 19:07:46 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 04:13:04 +08:00
* handles everything related to en / decryption
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name CryptTool
2017-01-14 22:29:12 +08:00
* @ class
2015-09-05 23:12:11 +08:00
* /
2018-12-30 01:40:59 +08:00
const CryptTool = ( function ( ) {
const me = { } ;
2017-02-09 03:12:22 +08:00
2019-05-16 03:20:54 +08:00
/ * *
* base58 encoder & decoder
*
* @ private
* /
let base58 = new baseX ( '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' ) ;
2018-09-02 01:42:22 +08:00
/ * *
* convert UTF - 8 string stored in a DOMString to a standard UTF - 16 DOMString
*
* Iterates over the bytes of the message , converting them all hexadecimal
* percent encoded representations , then URI decodes them all
*
2018-12-30 01:40:59 +08:00
* @ name CryptTool . utf8To16
2018-09-02 01:42:22 +08:00
* @ function
* @ private
* @ param { string } message UTF - 8 string
* @ return { string } UTF - 16 string
* /
2018-12-30 01:40:59 +08:00
function utf8To16 ( message )
2018-09-02 01:42:22 +08:00
{
return decodeURIComponent (
message . split ( '' ) . map (
function ( character )
{
return '%' + ( '00' + character . charCodeAt ( 0 ) . toString ( 16 ) ) . slice ( - 2 ) ;
}
) . join ( '' )
) ;
}
2015-09-05 23:12:11 +08:00
/ * *
2018-08-05 14:56:03 +08:00
* convert DOMString ( UTF - 16 ) to a UTF - 8 string stored in a DOMString
*
* URI encodes the message , then finds the percent encoded characters
* and transforms these hexadecimal representation back into bytes
*
2018-12-30 01:40:59 +08:00
* @ name CryptTool . utf16To8
2018-08-05 14:56:03 +08:00
* @ function
* @ private
* @ param { string } message UTF - 16 string
* @ return { string } UTF - 8 string
* /
2018-12-30 01:40:59 +08:00
function utf16To8 ( message )
2018-08-05 14:56:03 +08:00
{
return encodeURIComponent ( message ) . replace (
/%([0-9A-F]{2})/g ,
function ( match , hexCharacter )
{
return String . fromCharCode ( '0x' + hexCharacter ) ;
}
) ;
}
/ * *
2018-09-02 01:42:22 +08:00
* convert ArrayBuffer into a UTF - 8 string
2018-08-05 14:56:03 +08:00
*
2018-09-02 01:42:22 +08:00
* Iterates over the bytes of the array , catenating them into a string
2018-08-05 14:56:03 +08:00
*
2018-12-30 01:40:59 +08:00
* @ name CryptTool . arraybufferToString
2018-09-02 01:42:22 +08:00
* @ function
* @ private
* @ param { ArrayBuffer } messageArray
* @ return { string } message
* /
2018-12-30 01:40:59 +08:00
function arraybufferToString ( messageArray )
2018-09-02 01:42:22 +08:00
{
2018-12-30 01:40:59 +08:00
const array = new Uint8Array ( messageArray ) ;
let message = '' ,
i = 0 ;
while ( i < array . length ) {
message += String . fromCharCode ( array [ i ++ ] ) ;
2018-09-02 01:42:22 +08:00
}
return message ;
}
/ * *
* convert UTF - 8 string into a Uint8Array
*
* Iterates over the bytes of the message , writing them to the array
*
2018-12-30 01:40:59 +08:00
* @ name CryptTool . stringToArraybuffer
2018-08-05 14:56:03 +08:00
* @ function
* @ private
* @ param { string } message UTF - 8 string
2018-09-02 01:42:22 +08:00
* @ return { Uint8Array } array
2018-08-05 14:56:03 +08:00
* /
2018-12-30 01:40:59 +08:00
function stringToArraybuffer ( message )
2018-08-05 14:56:03 +08:00
{
2018-12-30 01:40:59 +08:00
const messageArray = new Uint8Array ( message . length ) ;
for ( let i = 0 ; i < message . length ; ++ i ) {
messageArray [ i ] = message . charCodeAt ( i ) ;
2018-09-02 01:42:22 +08:00
}
return messageArray ;
2018-08-05 14:56:03 +08:00
}
/ * *
2018-12-28 04:32:13 +08:00
* compress a string ( deflate compression ) , returns buffer
2015-09-05 23:12:11 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name CryptTool . compress
2018-12-28 04:32:13 +08:00
* @ async
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 01:08:08 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ param { string } message
2018-12-28 04:32:13 +08:00
* @ param { string } mode
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
* @ param { object } zlib
* @ throws { string }
2018-12-28 04:32:13 +08:00
* @ return { ArrayBuffer } data
2015-09-05 23:12:11 +08:00
* /
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
async function compress ( message , mode , zlib )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
message = stringToArraybuffer (
utf16To8 ( message )
2018-12-28 04:32:13 +08:00
) ;
if ( mode === 'zlib' ) {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
if ( typeof zlib === 'undefined' ) {
throw 'Error compressing paste, due to missing WebAssembly support.'
}
2019-08-27 13:38:27 +08:00
return zlib . deflate ( message ) . buffer ;
2018-08-05 14:56:03 +08:00
}
2018-12-28 04:32:13 +08:00
return message ;
2017-02-13 01:08:08 +08:00
}
2015-09-05 23:12:11 +08:00
/ * *
2018-12-28 04:32:13 +08:00
* decompress potentially base64 encoded , deflate compressed buffer , returns string
2015-09-05 23:12:11 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name CryptTool . decompress
2018-12-28 04:32:13 +08:00
* @ async
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 01:08:08 +08:00
* @ private
2018-12-28 04:32:13 +08:00
* @ param { ArrayBuffer } data
* @ param { string } mode
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
* @ param { object } zlib
* @ throws { string }
2017-01-14 22:29:12 +08:00
* @ return { string } message
2015-09-05 23:12:11 +08:00
* /
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
async function decompress ( data , mode , zlib )
2015-09-05 23:12:11 +08:00
{
2018-12-28 04:32:13 +08:00
if ( mode === 'zlib' || mode === 'none' ) {
if ( mode === 'zlib' ) {
2019-08-27 13:38:27 +08:00
if ( typeof zlib === 'undefined' ) {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
throw 'Error decompressing paste, due to missing WebAssembly support.'
2019-08-27 13:38:27 +08:00
}
data = zlib . inflate (
2018-12-30 01:40:59 +08:00
new Uint8Array ( data )
) . buffer ;
2018-12-28 04:32:13 +08:00
}
2018-12-30 01:40:59 +08:00
return utf8To16 (
arraybufferToString ( data )
2018-12-28 04:32:13 +08:00
) ;
}
2018-08-05 14:56:03 +08:00
// detect presence of Base64.js, indicating legacy ZeroBin paste
if ( typeof Base64 === 'undefined' ) {
2018-12-30 01:40:59 +08:00
return utf8To16 (
2018-12-28 04:32:13 +08:00
RawDeflate . inflate (
2018-12-30 01:40:59 +08:00
utf8To16 (
2018-12-28 04:32:13 +08:00
atob (
2018-12-30 01:40:59 +08:00
arraybufferToString ( data )
2018-12-28 04:32:13 +08:00
)
)
)
) ;
2018-08-05 14:56:03 +08:00
} else {
2018-12-28 04:32:13 +08:00
return Base64 . btou (
RawDeflate . inflate (
Base64 . fromBase64 (
2018-12-30 01:40:59 +08:00
arraybufferToString ( data )
2018-12-28 04:32:13 +08:00
)
)
) ;
2018-08-05 14:56:03 +08:00
}
2017-02-13 01:08:08 +08:00
}
2015-09-05 23:12:11 +08:00
2018-09-02 01:42:22 +08:00
/ * *
* returns specified number of random bytes
*
* @ name CryptTool . getRandomBytes
* @ function
* @ private
* @ param { int } length number of random bytes to fetch
* @ throws { string }
* @ return { string } random bytes
* /
function getRandomBytes ( length )
{
2019-06-22 21:44:54 +08:00
let bytes = '' ;
const byteArray = new Uint8Array ( length ) ;
window . crypto . getRandomValues ( byteArray ) ;
for ( let i = 0 ; i < length ; ++ i ) {
bytes += String . fromCharCode ( byteArray [ i ] ) ;
2018-09-02 01:42:22 +08:00
}
2019-06-22 21:44:54 +08:00
return bytes ;
2018-10-09 02:36:50 +08:00
}
2018-09-02 01:42:22 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2018-10-21 04:34:36 +08:00
* derive cryptographic key from key string and password
2015-09-05 23:12:11 +08:00
*
2018-10-21 04:34:36 +08:00
* @ name CryptTool . deriveKey
2018-10-20 23:57:21 +08:00
* @ async
2017-01-14 22:29:12 +08:00
* @ function
2018-10-21 04:34:36 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ param { string } key
* @ param { string } password
2018-12-26 00:34:39 +08:00
* @ param { array } spec cryptographic specification
2018-10-21 04:34:36 +08:00
* @ return { CryptoKey } derived key
2015-09-05 23:12:11 +08:00
* /
2018-12-26 00:34:39 +08:00
async function deriveKey ( key , password , spec )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
let keyArray = stringToArraybuffer ( key ) ;
2018-12-28 12:49:34 +08:00
if ( password . length > 0 ) {
// version 1 pastes did append the passwords SHA-256 hash in hex
if ( spec [ 7 ] === 'rawdeflate' ) {
let passwordBuffer = await window . crypto . subtle . digest (
{ name : 'SHA-256' } ,
2018-12-30 01:40:59 +08:00
stringToArraybuffer (
utf16To8 ( password )
)
2019-08-27 13:38:27 +08:00
) . catch ( Alert . showError ) ;
2018-12-28 12:49:34 +08:00
password = Array . prototype . map . call (
2018-12-30 01:40:59 +08:00
new Uint8Array ( passwordBuffer ) ,
x => ( '00' + x . toString ( 16 ) ) . slice ( - 2 )
2018-12-28 12:49:34 +08:00
) . join ( '' ) ;
}
2018-12-30 01:40:59 +08:00
let passwordArray = stringToArraybuffer ( password ) ,
2018-12-26 00:34:39 +08:00
newKeyArray = new Uint8Array ( keyArray . length + passwordArray . length ) ;
newKeyArray . set ( keyArray , 0 ) ;
newKeyArray . set ( passwordArray , keyArray . length ) ;
keyArray = newKeyArray ;
2015-09-05 23:12:11 +08:00
}
2018-09-02 01:42:22 +08:00
// import raw key
2018-10-21 01:53:21 +08:00
const importedKey = await window . crypto . subtle . importKey (
2018-09-02 01:42:22 +08:00
'raw' , // only 'raw' is allowed
2018-10-21 01:53:21 +08:00
keyArray ,
2018-09-02 01:42:22 +08:00
{ name : 'PBKDF2' } , // we use PBKDF2 for key derivation
false , // the key may not be exported
2018-10-09 02:36:50 +08:00
[ 'deriveKey' ] // we may only use it for key derivation
2019-08-27 13:38:27 +08:00
) . catch ( Alert . showError ) ;
2018-09-02 01:42:22 +08:00
// derive a stronger key for use with AES
2018-12-26 00:34:39 +08:00
return window . crypto . subtle . deriveKey (
2018-09-02 01:42:22 +08:00
{
name : 'PBKDF2' , // we use PBKDF2 for key derivation
2018-12-30 01:40:59 +08:00
salt : stringToArraybuffer ( spec [ 1 ] ) , // salt used in HMAC
2018-12-26 00:34:39 +08:00
iterations : spec [ 2 ] , // amount of iterations to apply
2018-10-09 02:36:50 +08:00
hash : { name : 'SHA-256' } // can be "SHA-1", "SHA-256", "SHA-384" or "SHA-512"
2018-09-02 01:42:22 +08:00
} ,
importedKey ,
{
2018-12-26 00:34:39 +08:00
name : 'AES-' + spec [ 6 ] . toUpperCase ( ) , // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
length : spec [ 3 ] // can be 128, 192 or 256
2018-09-02 01:42:22 +08:00
} ,
false , // the key may not be exported
2018-12-28 12:49:34 +08:00
[ 'encrypt' , 'decrypt' ] // we may only use it for en- and decryption
2019-08-27 13:38:27 +08:00
) . catch ( Alert . showError ) ;
2018-10-21 04:34:36 +08:00
}
2018-10-21 05:08:13 +08:00
/ * *
2018-12-26 00:34:39 +08:00
* gets crypto settings from specification and authenticated data
2018-10-21 05:08:13 +08:00
*
* @ name CryptTool . cryptoSettings
* @ function
* @ private
2018-12-26 00:34:39 +08:00
* @ param { string } adata authenticated data
* @ param { array } spec cryptographic specification
2018-10-21 05:08:13 +08:00
* @ return { object } crypto settings
* /
2018-12-26 00:34:39 +08:00
function cryptoSettings ( adata , spec )
2018-10-21 05:08:13 +08:00
{
return {
2018-12-26 00:34:39 +08:00
name : 'AES-' + spec [ 6 ] . toUpperCase ( ) , // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
2018-12-30 01:40:59 +08:00
iv : stringToArraybuffer ( spec [ 0 ] ) , // the initialization vector you used to encrypt
additionalData : stringToArraybuffer ( adata ) , // the addtional data you used during encryption (if any)
2018-12-26 00:34:39 +08:00
tagLength : spec [ 4 ] // the length of the tag you used to encrypt (if any)
2018-10-21 05:08:13 +08:00
} ;
}
2018-10-21 04:34:36 +08:00
/ * *
* compress , then encrypt message with given key and password
*
* @ name CryptTool . cipher
* @ async
* @ function
* @ param { string } key
* @ param { string } password
* @ param { string } message
2018-12-26 00:34:39 +08:00
* @ param { array } adata
2018-12-28 04:32:13 +08:00
* @ return { array } encrypted message in base64 encoding & adata containing encryption spec
2018-12-26 00:34:39 +08:00
* /
me . cipher = async function ( key , password , message , adata )
{
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
let zlib = ( await z ) ;
2018-12-26 00:34:39 +08:00
// AES in Galois Counter Mode, keysize 256 bit,
// authentication tag 128 bit, 10000 iterations in key derivation
2019-08-27 13:38:27 +08:00
const compression = (
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
typeof zlib === 'undefined' ?
2019-08-27 13:38:27 +08:00
'none' : // client lacks support for WASM
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
( $ ( 'body' ) . data ( 'compression' ) || 'zlib' )
2019-08-27 13:38:27 +08:00
) ,
spec = [
getRandomBytes ( 16 ) , // initialization vector
getRandomBytes ( 8 ) , // salt
100000 , // iterations
256 , // key size
128 , // tag size
'aes' , // algorithm
'gcm' , // algorithm mode
compression // compression
] , encodedSpec = [ ] ;
2018-12-28 04:32:13 +08:00
for ( let i = 0 ; i < spec . length ; ++ i ) {
encodedSpec [ i ] = i < 2 ? btoa ( spec [ i ] ) : spec [ i ] ;
}
2018-12-26 00:34:39 +08:00
if ( adata . length === 0 ) {
// comment
adata = encodedSpec ;
} else if ( adata [ 0 ] === null ) {
// paste
adata [ 0 ] = encodedSpec ;
}
2018-09-02 01:42:22 +08:00
// finally, encrypt message
2018-12-26 00:34:39 +08:00
return [
btoa (
2018-12-30 01:40:59 +08:00
arraybufferToString (
2018-12-26 00:34:39 +08:00
await window . crypto . subtle . encrypt (
cryptoSettings ( JSON . stringify ( adata ) , spec ) ,
await deriveKey ( key , password , spec ) ,
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
await compress ( message , compression , zlib )
2019-08-27 13:38:27 +08:00
) . catch ( Alert . showError )
2018-12-26 00:34:39 +08:00
)
) ,
adata
] ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-01-14 22:29:12 +08:00
* decrypt message with key , then decompress
2015-09-05 23:12:11 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name CryptTool . decipher
2018-10-20 23:57:21 +08:00
* @ async
2017-01-14 22:29:12 +08:00
* @ function
* @ param { string } key
* @ param { string } password
2018-12-26 00:34:39 +08:00
* @ param { string | object } data encrypted message
2018-01-06 20:32:07 +08:00
* @ return { string } decrypted message , empty if decryption failed
2015-09-05 23:12:11 +08:00
* /
2018-09-02 01:42:22 +08:00
me . decipher = async function ( key , password , data )
2015-09-05 23:12:11 +08:00
{
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
let adataString , spec , cipherMessage , plaintext ;
let zlib = ( await z ) ;
2018-12-26 00:34:39 +08:00
if ( data instanceof Array ) {
// version 2
adataString = JSON . stringify ( data [ 1 ] ) ;
2019-05-31 13:05:40 +08:00
// clone the array instead of passing the reference
spec = ( data [ 1 ] [ 0 ] instanceof Array ? data [ 1 ] [ 0 ] : data [ 1 ] ) . slice ( ) ;
2018-12-26 00:34:39 +08:00
cipherMessage = data [ 0 ] ;
} else if ( typeof data === 'string' ) {
// version 1
let object = JSON . parse ( data ) ;
adataString = atob ( object . adata ) ;
2019-05-31 13:05:40 +08:00
spec = [
2018-12-26 00:34:39 +08:00
object . iv ,
object . salt ,
object . iter ,
object . ks ,
object . ts ,
object . cipher ,
object . mode ,
'rawdeflate'
] ;
cipherMessage = object . ct ;
} else {
throw 'unsupported message format' ;
}
spec [ 0 ] = atob ( spec [ 0 ] ) ;
spec [ 1 ] = atob ( spec [ 1 ] ) ;
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
if ( spec [ 7 ] === 'zlib' ) {
if ( typeof zlib === 'undefined' ) {
throw 'Error decompressing paste, due to missing WebAssembly support.'
}
}
2018-09-02 01:42:22 +08:00
try {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
plaintext = await window . crypto . subtle . decrypt (
cryptoSettings ( adataString , spec ) ,
await deriveKey ( key , password , spec ) ,
stringToArraybuffer (
atob ( cipherMessage )
)
2018-10-21 04:05:35 +08:00
) ;
2018-09-02 01:42:22 +08:00
} catch ( err ) {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
console . error ( err ) ;
2018-09-02 01:42:22 +08:00
return '' ;
2015-09-01 03:14:12 +08:00
}
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
try {
return await decompress ( plaintext , spec [ 7 ] , zlib ) ;
} catch ( err ) {
Alert . showError ( err ) ;
return err ;
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-08 20:20:51 +08:00
2017-02-14 04:12:00 +08:00
/ * *
* returns a random symmetric key
*
2018-08-05 04:30:01 +08:00
* generates 256 bit long keys ( 8 Bits * 32 ) for AES with 256 bit long blocks
*
2017-02-15 05:21:55 +08:00
* @ name CryptTool . getSymmetricKey
2017-02-14 04:12:00 +08:00
* @ function
2018-08-05 04:30:01 +08:00
* @ throws { string }
2019-05-16 03:20:54 +08:00
* @ return { string } raw bytes
2017-02-14 04:12:00 +08:00
* /
2017-03-25 07:58:59 +08:00
me . getSymmetricKey = function ( )
2017-02-14 04:12:00 +08:00
{
2018-12-30 01:40:59 +08:00
return getRandomBytes ( 32 ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
2019-05-16 03:20:54 +08:00
* base58 encode a DOMString ( UTF - 16 )
2017-02-14 04:12:00 +08:00
*
2019-05-16 03:20:54 +08:00
* @ name CryptTool . base58encode
2017-02-14 04:12:00 +08:00
* @ function
2019-05-16 03:20:54 +08:00
* @ param { string } input
* @ return { string } output
2017-02-14 04:12:00 +08:00
* /
2019-05-16 03:20:54 +08:00
me . base58encode = function ( input )
2017-02-14 04:12:00 +08:00
{
2019-05-16 03:20:54 +08:00
return base58 . encode (
stringToArraybuffer ( input )
) ;
}
2017-02-14 04:12:00 +08:00
/ * *
2019-05-16 03:20:54 +08:00
* base58 decode a DOMString ( UTF - 16 )
2017-02-14 04:12:00 +08:00
*
2019-05-16 03:20:54 +08:00
* @ name CryptTool . base58decode
2017-02-14 04:12:00 +08:00
* @ function
2019-05-16 03:20:54 +08:00
* @ param { string } input
* @ return { string } output
2017-02-14 04:12:00 +08:00
* /
2019-05-16 03:20:54 +08:00
me . base58decode = function ( input )
2017-02-14 04:12:00 +08:00
{
2019-05-16 03:20:54 +08:00
return arraybufferToString (
base58 . decode ( input )
) ;
}
2017-02-14 04:12:00 +08:00
2017-02-09 03:12:22 +08:00
return me ;
2017-02-13 01:08:08 +08:00
} ) ( ) ;
2015-09-05 23:12:11 +08:00
2017-01-14 22:29:12 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* ( Model ) Data source ( aka MVC )
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Model
2017-01-14 22:29:12 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const Model = ( function ( ) {
const me = { } ;
2017-02-13 04:13:04 +08:00
2018-12-30 01:40:59 +08:00
let id = null ,
pasteData = null ,
symmetricKey = null ,
2017-02-18 03:46:10 +08:00
$templates ;
2017-02-13 04:13:04 +08:00
2015-09-28 02:34:39 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* returns the expiration set in the HTML
2017-01-14 22:29:12 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Model . getExpirationDefault
2017-02-15 05:21:55 +08:00
* @ function
* @ return string
2015-09-28 02:34:39 +08:00
* /
2017-02-15 05:21:55 +08:00
me . getExpirationDefault = function ( )
{
return $ ( '#pasteExpiration' ) . val ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-28 02:34:39 +08:00
2016-08-18 21:09:58 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* returns the format set in the HTML
2017-01-14 22:29:12 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Model . getFormatDefault
2017-02-15 05:21:55 +08:00
* @ function
* @ return string
2016-08-18 21:09:58 +08:00
* /
2017-02-15 05:21:55 +08:00
me . getFormatDefault = function ( )
{
return $ ( '#pasteFormatter' ) . val ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-08-18 21:09:58 +08:00
/ * *
2018-05-01 02:01:38 +08:00
* returns the paste data ( including the cipher data )
2017-01-14 22:29:12 +08:00
*
2017-04-11 22:34:13 +08:00
* @ name Model . getPasteData
2017-01-14 22:29:12 +08:00
* @ function
2017-04-11 22:34:13 +08:00
* @ param { function } callback ( optional ) Called when data is available
* @ param { function } useCache ( optional ) Whether to use the cache or
* force a data reload . Default : true
2017-02-13 04:13:04 +08:00
* @ return string
2015-09-05 23:12:11 +08:00
* /
2017-04-11 22:34:13 +08:00
me . getPasteData = function ( callback , useCache )
2015-09-05 23:12:11 +08:00
{
2017-04-11 22:34:13 +08:00
// use cache if possible/allowed
if ( useCache !== false && pasteData !== null ) {
//execute callback
if ( typeof callback === 'function' ) {
return callback ( pasteData ) ;
}
// alternatively just using inline
return pasteData ;
}
// reload data
2018-12-26 00:34:39 +08:00
ServerInteraction . prepare ( ) ;
2019-06-02 05:49:40 +08:00
ServerInteraction . setUrl ( Helper . baseUri ( ) + '?pasteid=' + me . getPasteId ( ) ) ;
2017-04-11 22:34:13 +08:00
2018-12-26 00:34:39 +08:00
ServerInteraction . setFailure ( function ( status , data ) {
2017-04-11 22:34:13 +08:00
// revert loading status…
Alert . hideLoading ( ) ;
TopNav . showViewButtons ( ) ;
// show error message
2018-12-26 00:34:39 +08:00
Alert . showError ( ServerInteraction . parseUploadError ( status , data , 'get paste data' ) ) ;
2018-05-22 17:43:44 +08:00
} ) ;
2018-12-26 00:34:39 +08:00
ServerInteraction . setSuccess ( function ( status , data ) {
2019-05-25 19:20:39 +08:00
pasteData = new Paste ( data ) ;
2017-04-11 22:34:13 +08:00
if ( typeof callback === 'function' ) {
2019-05-25 19:20:39 +08:00
return callback ( pasteData ) ;
2017-04-11 22:34:13 +08:00
}
2018-05-22 17:43:44 +08:00
} ) ;
2018-12-26 00:34:39 +08:00
ServerInteraction . run ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2015-09-12 23:33:16 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* get the pastes unique identifier from the URL ,
2017-04-11 22:34:13 +08:00
* eg . https : //example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
2015-09-12 23:33:16 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Model . getPasteId
2017-01-14 22:29:12 +08:00
* @ function
2017-02-15 05:21:55 +08:00
* @ return { string } unique identifier
2017-02-18 03:46:10 +08:00
* @ throws { string }
2015-09-12 23:33:16 +08:00
* /
2017-02-15 05:21:55 +08:00
me . getPasteId = function ( )
2015-09-12 23:33:16 +08:00
{
2019-01-22 07:07:28 +08:00
const idRegEx = /^[a-z0-9]{16}$/ ;
2017-02-18 03:46:10 +08:00
2019-01-22 07:07:28 +08:00
// return cached value
if ( id !== null ) {
return id ;
}
// do use URL interface, if possible
2019-06-21 04:30:49 +08:00
const url = new URL ( window . location ) ;
2019-01-22 07:07:28 +08:00
2019-06-21 04:30:49 +08:00
for ( const param of url . searchParams ) {
const key = param [ 0 ] ;
const value = param [ 1 ] ;
2019-01-22 07:07:28 +08:00
2019-06-21 04:30:49 +08:00
if ( value === '' && idRegEx . test ( key ) ) {
// safe, as the whole regex is matched
id = key ;
return key ;
2017-02-18 03:46:10 +08:00
}
2019-01-22 07:07:28 +08:00
}
2019-06-21 04:30:49 +08:00
if ( id === null ) {
2019-01-22 07:07:28 +08:00
throw 'no paste id given' ;
2015-09-12 23:33:16 +08:00
}
2017-02-15 05:21:55 +08:00
return id ;
2018-05-22 17:41:35 +08:00
}
/ * *
2018-12-30 01:40:59 +08:00
* returns true , when the URL has a delete token and the current call was used for deleting a paste .
2018-05-22 17:41:35 +08:00
*
* @ name Model . hasDeleteToken
* @ function
* @ return { bool }
* /
me . hasDeleteToken = function ( )
{
return window . location . search . indexOf ( 'deletetoken' ) !== - 1 ;
}
2015-09-12 23:33:16 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* return the deciphering key stored in anchor part of the URL
2015-09-05 23:12:11 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Model . getPasteKey
2017-01-14 22:29:12 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ return { string | null } key
* @ throws { string }
2015-09-05 23:12:11 +08:00
* /
2017-02-15 05:21:55 +08:00
me . getPasteKey = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-15 05:21:55 +08:00
if ( symmetricKey === null ) {
2018-12-30 01:40:59 +08:00
let newKey = window . location . hash . substring ( 1 ) ;
if ( newKey === '' ) {
2017-02-18 03:46:10 +08:00
throw 'no encryption key given' ;
}
2017-02-15 05:21:55 +08:00
// Some web 2.0 services and redirectors add data AFTER the anchor
// (such as &utm_source=...). We will strip any additional data.
2018-12-30 01:40:59 +08:00
let ampersandPos = newKey . indexOf ( '&' ) ;
2017-02-15 05:21:55 +08:00
if ( ampersandPos > - 1 )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
newKey = newKey . substring ( 0 , ampersandPos ) ;
2017-02-15 05:21:55 +08:00
}
2018-12-30 01:40:59 +08:00
2019-05-19 14:36:18 +08:00
// version 2 uses base58, version 1 uses base64 without decoding
2019-05-16 03:20:54 +08:00
try {
2019-05-19 15:54:40 +08:00
// base58 encode strips NULL bytes at the beginning of the
// string, so we re-add them if necessary
symmetricKey = CryptTool . base58decode ( newKey ) . padStart ( 32 , '\u0000' ) ;
2019-05-16 03:20:54 +08:00
} catch ( e ) {
2019-05-19 14:36:18 +08:00
symmetricKey = newKey ;
2017-02-15 05:21:55 +08:00
}
}
2015-09-18 18:33:10 +08:00
2017-02-15 05:21:55 +08:00
return symmetricKey ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* returns a jQuery copy of the HTML template
2015-09-05 23:12:11 +08:00
*
2017-02-18 03:46:10 +08:00
* @ name Model . getTemplate
2017-01-14 22:29:12 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ param { string } name - the name of the template
* @ return { jQuery }
2015-09-05 23:12:11 +08:00
* /
2017-02-18 03:46:10 +08:00
me . getTemplate = function ( name )
2015-09-05 23:12:11 +08:00
{
2017-02-18 03:46:10 +08:00
// find template
2018-12-30 01:40:59 +08:00
let $element = $templates . find ( '#' + name + 'template' ) . clone ( true ) ;
2017-02-18 03:46:10 +08:00
// change ID to avoid collisions (one ID should really be unique)
return $element . prop ( 'id' , name ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-25 16:35:55 +08:00
* resets state , used for unit testing
2015-09-05 23:12:11 +08:00
*
2017-02-25 16:35:55 +08:00
* @ name Model . reset
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-25 16:35:55 +08:00
me . reset = function ( )
2015-09-05 23:12:11 +08:00
{
2017-04-11 22:34:13 +08:00
pasteData = $templates = id = symmetricKey = null ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 04:13:04 +08:00
* init navigation manager
2015-09-05 23:12:11 +08:00
*
2017-02-13 04:13:04 +08:00
* preloads jQuery elements
*
2017-02-16 05:59:55 +08:00
* @ name Model . init
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-13 04:13:04 +08:00
me . init = function ( )
2015-09-02 04:33:07 +08:00
{
2017-02-18 03:46:10 +08:00
$templates = $ ( '#templates' ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2017-02-13 04:13:04 +08:00
return me ;
2017-02-25 16:35:55 +08:00
} ) ( ) ;
2017-02-07 03:16:03 +08:00
2017-02-09 03:11:04 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* Helper functions for user interface
*
* everything directly UI - related , which fits nowhere else
2017-02-09 03:11:04 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name UiHelper
2017-02-09 03:12:22 +08:00
* @ class
2017-02-09 03:11:04 +08:00
* /
2018-12-30 01:40:59 +08:00
const UiHelper = ( function ( ) {
const me = { } ;
2015-09-17 04:51:48 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* handle history ( pop ) state changes
*
* currently this does only handle redirects to the home page .
2015-09-17 04:51:48 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name UiHelper . historyChange
2017-02-15 05:21:55 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 18:35:04 +08:00
* @ param { Event } event
2015-09-17 04:51:48 +08:00
* /
2017-02-15 05:21:55 +08:00
function historyChange ( event )
2015-09-17 04:51:48 +08:00
{
2018-12-30 01:40:59 +08:00
let currentLocation = Helper . baseUri ( ) ;
2017-02-13 18:35:04 +08:00
if ( event . originalEvent . state === null && // no state object passed
2017-10-22 15:56:44 +08:00
event . target . location . href === currentLocation && // target location is home page
2017-02-13 18:35:04 +08:00
window . location . href === currentLocation // and we are not already on the home page
) {
// redirect to home page
window . location . href = currentLocation ;
2015-09-17 04:51:48 +08:00
}
2017-02-25 16:35:55 +08:00
}
2012-04-22 03:59:45 +08:00
2016-01-31 16:56:06 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* reload the page
2016-01-31 16:56:06 +08:00
*
2017-02-15 05:21:55 +08:00
* This takes the user to the PrivateBin homepage .
2017-02-13 18:35:04 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name UiHelper . reloadHome
2017-01-14 22:29:12 +08:00
* @ function
2016-01-31 16:56:06 +08:00
* /
2017-02-15 05:21:55 +08:00
me . reloadHome = function ( )
2016-01-31 16:56:06 +08:00
{
2017-02-15 05:21:55 +08:00
window . location . href = Helper . baseUri ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-08 20:20:51 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* checks whether the element is currently visible in the viewport ( so
* the user can actually see it )
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ see { @ link https : //stackoverflow.com/a/40658647}
2017-02-18 03:46:10 +08:00
* @ name UiHelper . isVisible
2017-01-14 22:29:12 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ param { jQuery } $element The link hash to move to .
2015-09-05 23:12:11 +08:00
* /
2017-02-18 03:46:10 +08:00
me . isVisible = function ( $element )
2015-09-05 23:12:11 +08:00
{
2018-12-30 01:40:59 +08:00
let elementTop = $element . offset ( ) . top ,
viewportTop = $ ( window ) . scrollTop ( ) ,
viewportBottom = viewportTop + $ ( window ) . height ( ) ;
2018-02-22 05:51:31 +08:00
return elementTop > viewportTop && elementTop < viewportBottom ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2017-02-06 05:09:46 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* scrolls to a specific element
2017-02-06 05:09:46 +08:00
*
2017-03-14 03:24:18 +08:00
* @ see { @ link https : //stackoverflow.com/questions/4198041/jquery-smooth-scroll-to-an-anchor#answer-12714767}
2017-02-18 03:46:10 +08:00
* @ name UiHelper . scrollTo
2017-02-06 05:09:46 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ param { jQuery } $element The link hash to move to .
* @ param { ( number | string ) } animationDuration passed to jQuery . animate , when set to 0 the animation is skipped
* @ param { string } animationEffect passed to jQuery . animate
* @ param { function } finishedCallback function to call after animation finished
2017-02-06 05:09:46 +08:00
* /
2017-02-18 03:46:10 +08:00
me . scrollTo = function ( $element , animationDuration , animationEffect , finishedCallback )
2017-02-06 05:09:46 +08:00
{
2018-12-30 01:40:59 +08:00
let $body = $ ( 'html, body' ) ,
2017-02-18 03:46:10 +08:00
margin = 50 ,
2018-12-30 01:40:59 +08:00
callbackCalled = false ,
dest = 0 ;
2017-02-18 03:46:10 +08:00
2018-12-30 01:40:59 +08:00
// calculate destination place
2017-02-18 03:46:10 +08:00
// if it would scroll out of the screen at the bottom only scroll it as
// far as the screen can go
if ( $element . offset ( ) . top > $ ( document ) . height ( ) - $ ( window ) . height ( ) ) {
dest = $ ( document ) . height ( ) - $ ( window ) . height ( ) ;
} else {
dest = $element . offset ( ) . top - margin ;
}
// skip animation if duration is set to 0
if ( animationDuration === 0 ) {
window . scrollTo ( 0 , dest ) ;
} else {
// stop previous animation
$body . stop ( ) ;
// scroll to destination
$body . animate ( {
scrollTop : dest
} , animationDuration , animationEffect ) ;
}
// as we have finished we can enable scrolling again
$body . queue ( function ( next ) {
if ( ! callbackCalled ) {
// call user function if needed
if ( typeof finishedCallback !== 'undefined' ) {
finishedCallback ( ) ;
}
2017-02-06 05:09:46 +08:00
2017-02-18 03:46:10 +08:00
// prevent calling this function twice
callbackCalled = true ;
}
next ( ) ;
} ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-06 05:09:46 +08:00
2017-10-22 15:56:44 +08:00
/ * *
* trigger a history ( pop ) state change
*
* used to test the UiHelper . historyChange private function
*
* @ name UiHelper . mockHistoryChange
* @ function
2017-10-22 16:39:18 +08:00
* @ param { string } state ( optional ) state to mock
2017-10-22 15:56:44 +08:00
* /
2017-10-22 16:39:18 +08:00
me . mockHistoryChange = function ( state )
2017-10-22 15:56:44 +08:00
{
2017-10-22 16:39:18 +08:00
if ( typeof state === 'undefined' ) {
state = null ;
}
historyChange ( $ . Event ( 'popstate' , { originalEvent : new PopStateEvent ( 'popstate' , { state : state } ) , target : window } ) ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-06 05:09:46 +08:00
2017-02-07 05:39:45 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* initialize
2017-02-07 05:39:45 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name UiHelper . init
2017-02-07 05:39:45 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . init = function ( )
2017-02-07 05:39:45 +08:00
{
2017-02-15 05:21:55 +08:00
// update link to home page
$ ( '.reloadlink' ) . prop ( 'href' , Helper . baseUri ( ) ) ;
2017-02-13 18:35:04 +08:00
2017-02-15 05:21:55 +08:00
$ ( window ) . on ( 'popstate' , historyChange ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-12 23:33:16 +08:00
2017-02-13 18:35:04 +08:00
return me ;
2017-10-22 19:39:23 +08:00
} ) ( ) ;
2017-02-07 05:39:45 +08:00
2017-02-13 18:35:04 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* Alert / error manager
2017-02-13 18:35:04 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Alert
2017-02-13 18:35:04 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const Alert = ( function ( ) {
const me = { } ;
2017-02-13 18:35:04 +08:00
2018-12-30 01:40:59 +08:00
let $errorMessage ,
2017-02-18 05:46:18 +08:00
$loadingIndicator ,
2017-03-12 21:16:08 +08:00
$statusMessage ,
2018-12-30 01:40:59 +08:00
$remainingTime ,
currentIcon ,
customHandler ;
2017-02-16 05:59:55 +08:00
2018-12-30 01:40:59 +08:00
const alertType = [
'loading' , // not in bootstrap CSS, but using a plausible value here
'info' , // status icon
2019-08-29 02:29:23 +08:00
'warning' , // warning icon
2018-12-30 01:40:59 +08:00
'danger' // error icon
2017-02-18 03:46:10 +08:00
] ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* forwards a request to the i18n module and shows the element
2016-07-11 17:09:41 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Alert . handleNotification
2017-02-16 05:59:55 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { int } id - id of notification
* @ param { jQuery } $element - jQuery object
* @ param { string | array } args
* @ param { string | null } icon - optional , icon
2015-09-05 23:12:11 +08:00
* /
2017-02-16 05:59:55 +08:00
function handleNotification ( id , $element , args , icon )
{
2017-02-18 03:46:10 +08:00
// basic parsing/conversion of parameters
if ( typeof icon === 'undefined' ) {
icon = null ;
}
if ( typeof args === 'undefined' ) {
args = null ;
} else if ( typeof args === 'string' ) {
// convert string to array if needed
args = [ args ] ;
2019-06-19 01:45:52 +08:00
} else if ( args instanceof Error ) {
// extract message into array if needed
args = [ args . message ] ;
2017-02-18 03:46:10 +08:00
}
2017-03-12 21:16:08 +08:00
// pass to custom handler if defined
2017-02-18 03:46:10 +08:00
if ( typeof customHandler === 'function' ) {
2018-12-30 01:40:59 +08:00
let handlerResult = customHandler ( alertType [ id ] , $element , args , icon ) ;
2017-02-18 03:46:10 +08:00
if ( handlerResult === true ) {
2018-01-02 22:38:37 +08:00
// if it returns true, skip own handler
2017-02-18 03:46:10 +08:00
return ;
2016-07-11 17:09:41 +08:00
}
2017-02-18 03:46:10 +08:00
if ( handlerResult instanceof jQuery ) {
// continue processing with new element
$element = handlerResult ;
icon = null ; // icons not supported in this case
}
}
2019-08-29 02:29:23 +08:00
let $translationTarget = $element ;
// handle icon, if template uses one
const $glyphIcon = $element . find ( ':first' ) ;
if ( $glyphIcon . length ) {
// if there is an icon, we need to provide an inner element
// to translate the message into, instead of the parent
$translationTarget = $ ( '<span>' ) ;
$element . html ( ' ' ) . prepend ( $glyphIcon ) . append ( $translationTarget ) ;
if ( icon !== null && // icon was passed
icon !== currentIcon [ id ] // and it differs from current icon
) {
// remove (previous) icon
$glyphIcon . removeClass ( currentIcon [ id ] ) ;
// any other thing as a string (e.g. 'null') (only) removes the icon
if ( typeof icon === 'string' ) {
// set new icon
currentIcon [ id ] = 'glyphicon-' + icon ;
$glyphIcon . addClass ( currentIcon [ id ] ) ;
}
2016-07-11 17:09:41 +08:00
}
2017-02-16 05:59:55 +08:00
}
2016-07-11 17:09:41 +08:00
2017-02-16 05:59:55 +08:00
// show text
2017-02-18 03:46:10 +08:00
if ( args !== null ) {
2017-03-13 00:08:12 +08:00
// add jQuery object to it as first parameter
2019-08-29 02:29:23 +08:00
args . unshift ( $translationTarget ) ;
2017-03-13 00:08:12 +08:00
// pass it to I18n
I18n . _ . apply ( this , args ) ;
2015-09-05 23:12:11 +08:00
}
2017-02-16 05:59:55 +08:00
// show notification
$element . removeClass ( 'hidden' ) ;
}
2015-09-05 23:12:11 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* display a status message
2017-02-09 03:12:22 +08:00
*
2017-02-16 05:59:55 +08:00
* This automatically passes the text to I18n for translation .
2017-01-14 22:29:12 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Alert . showStatus
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { string | array } message string , use an array for % s / % d options
* @ param { string | null } icon optional , the icon to show ,
* default : leave previous icon
2015-09-05 23:12:11 +08:00
* /
2018-01-06 20:32:07 +08:00
me . showStatus = function ( message , icon )
2015-09-05 23:12:11 +08:00
{
2017-02-16 05:59:55 +08:00
handleNotification ( 1 , $statusMessage , message , icon ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2019-08-27 13:38:27 +08:00
/ * *
* display a warning message
*
* This automatically passes the text to I18n for translation .
*
* @ name Alert . showWarning
* @ function
* @ param { string | array } message string , use an array for % s / % d options
* @ param { string | null } icon optional , the icon to show , default :
* leave previous icon
* /
me . showWarning = function ( message , icon )
{
2019-08-29 02:29:23 +08:00
$errorMessage . find ( ':first' )
. removeClass ( currentIcon [ 3 ] )
. addClass ( currentIcon [ 2 ] ) ;
2019-08-27 13:38:27 +08:00
handleNotification ( 2 , $errorMessage , message , icon ) ;
} ;
2016-08-11 17:31:34 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* display an error message
2017-01-14 22:29:12 +08:00
*
2017-02-16 05:59:55 +08:00
* This automatically passes the text to I18n for translation .
*
* @ name Alert . showError
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { string | array } message string , use an array for % s / % d options
* @ param { string | null } icon optional , the icon to show , default :
* leave previous icon
2016-08-11 17:31:34 +08:00
* /
2018-01-06 20:32:07 +08:00
me . showError = function ( message , icon )
2016-08-11 17:31:34 +08:00
{
2017-02-16 05:59:55 +08:00
handleNotification ( 3 , $errorMessage , message , icon ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-08-11 17:31:34 +08:00
2016-08-18 21:09:58 +08:00
/ * *
2017-03-12 21:16:08 +08:00
* display remaining message
*
* This automatically passes the text to I18n for translation .
2016-08-18 21:09:58 +08:00
*
2017-03-12 21:16:08 +08:00
* @ name Alert . showRemaining
2017-01-14 22:29:12 +08:00
* @ function
2017-03-12 21:16:08 +08:00
* @ param { string | array } message string , use an array for % s / % d options
2016-08-18 21:09:58 +08:00
* /
2017-03-12 21:16:08 +08:00
me . showRemaining = function ( message )
2016-08-18 21:09:58 +08:00
{
2017-03-12 21:16:08 +08:00
handleNotification ( 1 , $remainingTime , message ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-08-18 21:09:58 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* shows a loading message , optionally with a percentage
2015-09-05 23:12:11 +08:00
*
2017-02-16 05:59:55 +08:00
* This automatically passes all texts to the i10s module .
2017-02-07 03:39:52 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Alert . showLoading
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { string | array | null } message optional , use an array for % s / % d options , default : 'Loading…'
* @ param { string | null } icon optional , the icon to show , default : leave previous icon
2015-09-05 23:12:11 +08:00
* /
2018-01-06 20:32:07 +08:00
me . showLoading = function ( message , icon )
2015-09-05 23:12:11 +08:00
{
2017-02-16 05:59:55 +08:00
// default message text
if ( typeof message === 'undefined' ) {
message = 'Loading…' ;
}
handleNotification ( 0 , $loadingIndicator , message , icon ) ;
2017-02-18 03:46:10 +08:00
// show loading status (cursor)
$ ( 'body' ) . addClass ( 'loading' ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* hides the loading message
2015-09-05 23:12:11 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Alert . hideLoading
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-16 05:59:55 +08:00
me . hideLoading = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-16 05:59:55 +08:00
$loadingIndicator . addClass ( 'hidden' ) ;
2017-02-18 03:46:10 +08:00
// hide loading cursor
$ ( 'body' ) . removeClass ( 'loading' ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* hides any status / error messages
*
* This does not include the loading message .
2015-09-05 23:12:11 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Alert . hideMessages
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-16 05:59:55 +08:00
me . hideMessages = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-16 05:59:55 +08:00
$statusMessage . addClass ( 'hidden' ) ;
$errorMessage . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2012-04-22 03:59:45 +08:00
2016-08-09 20:46:32 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* set a custom handler , which gets all notifications .
*
* This handler gets the following arguments :
* alertType ( see array ) , $element , args , icon
* If it returns true , the own processing will be stopped so the message
* will not be displayed . Otherwise it will continue .
* As an aditional feature it can return q jQuery element , which will
* then be used to add the message there . Icons are not supported in
* that case and will be ignored .
* Pass 'null' to reset / delete the custom handler .
* Note that there is no notification when a message is supposed to get
* hidden .
*
* @ name Alert . setCustomHandler
2017-01-14 22:29:12 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ param { function | null } newHandler
2016-08-09 20:46:32 +08:00
* /
2017-02-18 03:46:10 +08:00
me . setCustomHandler = function ( newHandler )
2016-08-09 20:46:32 +08:00
{
2017-02-18 03:46:10 +08:00
customHandler = newHandler ;
2018-01-06 16:26:10 +08:00
} ;
2016-08-09 20:46:32 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* init status manager
2017-02-13 18:35:04 +08:00
*
2017-02-15 05:21:55 +08:00
* preloads jQuery elements
2016-08-09 20:46:32 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Alert . init
2017-01-14 22:29:12 +08:00
* @ function
2016-08-09 20:46:32 +08:00
* /
2017-02-15 05:21:55 +08:00
me . init = function ( )
2016-08-09 20:46:32 +08:00
{
2017-02-18 03:46:10 +08:00
// hide "no javascript" error message
2017-02-15 05:21:55 +08:00
$ ( '#noscript' ) . hide ( ) ;
2016-08-11 17:40:37 +08:00
2017-02-18 03:46:10 +08:00
// not a reset, but first set of the elements
2017-02-15 05:21:55 +08:00
$errorMessage = $ ( '#errormessage' ) ;
2017-02-16 05:59:55 +08:00
$loadingIndicator = $ ( '#loadingindicator' ) ;
2017-02-18 05:46:18 +08:00
$statusMessage = $ ( '#status' ) ;
2017-03-12 21:16:08 +08:00
$remainingTime = $ ( '#remainingtime' ) ;
2017-10-24 03:33:07 +08:00
currentIcon = [
'glyphicon-time' , // loading icon
'glyphicon-info-sign' , // status icon
2019-08-27 13:38:27 +08:00
'glyphicon-warning-sign' , // warning icon
2017-10-24 03:33:07 +08:00
'glyphicon-alert' // error icon
] ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2017-02-15 05:21:55 +08:00
return me ;
2017-03-25 07:58:59 +08:00
} ) ( ) ;
2017-02-15 05:21:55 +08:00
/ * *
* handles paste status / result
*
2017-03-14 03:24:18 +08:00
* @ name PasteStatus
2017-02-15 05:21:55 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const PasteStatus = ( function ( ) {
const me = { } ;
2017-02-15 05:21:55 +08:00
2018-12-30 01:40:59 +08:00
let $pasteSuccess ,
2017-02-15 05:21:55 +08:00
$pasteUrl ,
2017-02-18 05:46:18 +08:00
$remainingTime ,
$shortenButton ;
2016-08-09 20:46:32 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* forward to URL shortener
2016-08-09 20:46:32 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteStatus . sendToShortener
2017-02-14 04:12:00 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ function
2016-08-09 20:46:32 +08:00
* /
2018-01-06 20:32:07 +08:00
function sendToShortener ( )
2016-08-09 20:46:32 +08:00
{
2019-08-14 09:44:53 +08:00
if ( $shortenButton . hasClass ( 'buttondisabled' ) ) {
return ;
}
$ . ajax ( {
type : 'GET' ,
url : ` ${ $shortenButton . data ( 'shortener' ) } ${ encodeURIComponent ( $pasteUrl . attr ( 'href' ) ) } ` ,
headers : { 'Accept' : 'text/html, application/xhtml+xml, application/xml, application/json' } ,
processData : false ,
timeout : 10000 ,
xhrFields : {
withCredentials : false
} ,
success : function ( response ) {
let responseString = response ;
if ( typeof responseString === 'object' ) {
responseString = JSON . stringify ( responseString ) ;
}
if ( typeof responseString === 'string' && responseString . length > 0 ) {
const shortUrlMatcher = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g ;
2019-08-14 12:02:25 +08:00
const shortUrl = ( responseString . match ( shortUrlMatcher ) || [ ] ) . sort ( function ( a , b ) {
2019-08-14 09:44:53 +08:00
return a . length - b . length ;
} ) [ 0 ] ;
if ( typeof shortUrl === 'string' && shortUrl . length > 0 ) {
// we disable the button to avoid calling shortener again
$shortenButton . addClass ( 'buttondisabled' ) ;
2020-05-30 18:07:47 +08:00
// update link
$pasteUrl . text ( shortUrl ) ;
$pasteUrl . prop ( 'href' , shortUrl ) ;
2019-08-14 09:44:53 +08:00
// we pre-select the link so that the user only has to [Ctrl]+[c] the link
Helper . selectText ( $pasteUrl [ 0 ] ) ;
return ;
}
}
2019-08-27 13:38:27 +08:00
Alert . showError ( 'Cannot parse response from URL shortener.' ) ;
2019-08-14 09:44:53 +08:00
}
} )
. fail ( function ( data , textStatus , errorThrown ) {
console . error ( textStatus , errorThrown ) ;
2019-08-27 13:38:27 +08:00
// we don't know why it failed, could be CORS of the external
// server not setup properly, in which case we follow old
// behavior to open it in new tab
2019-08-14 09:44:53 +08:00
window . open (
` ${ $shortenButton . data ( 'shortener' ) } ${ encodeURIComponent ( $pasteUrl . attr ( 'href' ) ) } ` ,
'_blank' ,
'noopener, noreferrer'
2019-08-29 23:21:56 +08:00
) ;
2019-08-14 09:44:53 +08:00
} ) ;
2017-02-14 04:12:00 +08:00
}
/ * *
2017-02-15 05:21:55 +08:00
* Forces opening the paste if the link does not do this automatically .
*
* This is necessary as browsers will not reload the page when it is
* already loaded ( which is fake as it is set via history . pushState ( ) ) .
2017-02-14 04:12:00 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteStatus . pasteLinkClick
2017-02-15 05:21:55 +08:00
* @ function
* /
2018-01-06 20:32:07 +08:00
function pasteLinkClick ( )
2017-02-15 05:21:55 +08:00
{
// check if location is (already) shown in URL bar
if ( window . location . href === $pasteUrl . attr ( 'href' ) ) {
// if so we need to load link by reloading the current site
window . location . reload ( true ) ;
}
}
/ * *
* creates a notification after a successfull paste upload
2017-02-14 04:12:00 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name PasteStatus . createPasteNotification
2017-02-14 04:12:00 +08:00
* @ function
* @ param { string } url
* @ param { string } deleteUrl
* /
me . createPasteNotification = function ( url , deleteUrl )
2017-02-13 01:08:08 +08:00
{
2020-01-18 17:44:35 +08:00
I18n . _ (
$ ( '#pastelink' ) ,
'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>' ,
url , url
2017-02-14 04:12:00 +08:00
) ;
// save newly created element
$pasteUrl = $ ( '#pasteurl' ) ;
// and add click event
$pasteUrl . click ( pasteLinkClick ) ;
2019-08-14 09:44:53 +08:00
// delete link
2020-01-25 15:13:36 +08:00
$ ( '#deletelink' ) . html ( '<a href="' + deleteUrl + '"></a>' ) ;
I18n . _ ( $ ( '#deletelink a' ) . first ( ) , 'Delete data' ) ;
2017-02-13 18:35:04 +08:00
2019-08-15 08:36:44 +08:00
// enable shortener button
$shortenButton . removeClass ( 'buttondisabled' ) ;
2017-02-14 04:12:00 +08:00
// show result
$pasteSuccess . removeClass ( 'hidden' ) ;
// we pre-select the link so that the user only has to [Ctrl]+[c] the link
2017-02-15 05:21:55 +08:00
Helper . selectText ( $pasteUrl [ 0 ] ) ;
2018-01-06 16:26:10 +08:00
} ;
2012-04-22 03:59:45 +08:00
2017-02-14 04:12:00 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* shows the remaining time
2017-02-14 04:12:00 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name PasteStatus . showRemainingTime
2017-02-14 04:12:00 +08:00
* @ function
2019-05-25 19:20:39 +08:00
* @ param { Paste } paste
2017-02-14 04:12:00 +08:00
* /
2019-05-15 13:44:03 +08:00
me . showRemainingTime = function ( paste )
2017-02-14 04:12:00 +08:00
{
2019-05-25 19:20:39 +08:00
if ( paste . isBurnAfterReadingEnabled ( ) ) {
2017-02-15 05:21:55 +08:00
// display paste "for your eyes only" if it is deleted
2017-04-13 16:46:09 +08:00
// the paste has been deleted when the JSON with the ciphertext
2017-04-11 22:34:13 +08:00
// has been downloaded
2017-02-15 05:21:55 +08:00
2018-10-09 02:36:50 +08:00
Alert . showRemaining ( 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.' ) ;
2017-02-15 05:21:55 +08:00
$remainingTime . addClass ( 'foryoureyesonly' ) ;
2019-05-25 19:20:39 +08:00
} else if ( paste . getTimeToLive ( ) > 0 ) {
2017-02-15 05:21:55 +08:00
// display paste expiration
2019-05-25 19:20:39 +08:00
let expiration = Helper . secondsToHuman ( paste . getTimeToLive ( ) ) ,
2017-02-15 05:21:55 +08:00
expirationLabel = [
'This document will expire in %d ' + expiration [ 1 ] + '.' ,
'This document will expire in %d ' + expiration [ 1 ] + 's.'
] ;
2017-03-13 00:08:12 +08:00
Alert . showRemaining ( [ expirationLabel , expiration [ 0 ] ] ) ;
2017-11-14 13:52:12 +08:00
$remainingTime . removeClass ( 'foryoureyesonly' ) ;
2017-02-15 05:21:55 +08:00
} else {
// never expires
return ;
2017-02-14 04:12:00 +08:00
}
2017-02-15 05:21:55 +08:00
// in the end, display notification
$remainingTime . removeClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* hides the remaining time and successful upload notification
*
2018-03-01 13:43:30 +08:00
* @ name PasteStatus . hideMessages
2017-02-16 05:59:55 +08:00
* @ function
* /
me . hideMessages = function ( )
{
$remainingTime . addClass ( 'hidden' ) ;
$pasteSuccess . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-16 05:59:55 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* init status manager
2017-02-09 03:12:22 +08:00
*
2017-02-13 18:35:04 +08:00
* preloads jQuery elements
*
2017-03-14 03:24:18 +08:00
* @ name PasteStatus . init
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 01:08:08 +08:00
me . init = function ( )
2017-02-09 03:12:22 +08:00
{
2017-11-16 15:57:08 +08:00
$pasteSuccess = $ ( '#pastesuccess' ) ;
2017-02-15 05:21:55 +08:00
// $pasteUrl is saved in me.createPasteNotification() after creation
$remainingTime = $ ( '#remainingtime' ) ;
2017-02-18 05:46:18 +08:00
$shortenButton = $ ( '#shortenbutton' ) ;
2017-02-09 03:12:22 +08:00
2017-02-14 04:12:00 +08:00
// bind elements
$shortenButton . click ( sendToShortener ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-08 20:20:51 +08:00
2017-02-13 01:08:08 +08:00
return me ;
2017-11-14 04:57:49 +08:00
} ) ( ) ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* password prompt
2017-02-13 01:08:08 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Prompt
2017-02-13 01:08:08 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const Prompt = ( function ( ) {
const me = { } ;
2017-02-13 01:08:08 +08:00
2018-12-30 01:40:59 +08:00
let $passwordDecrypt ,
2017-02-13 18:35:04 +08:00
$passwordForm ,
2018-12-30 01:40:59 +08:00
$passwordModal ,
password = '' ;
2017-02-15 05:21:55 +08:00
2017-03-14 03:24:18 +08:00
/ * *
* submit a password in the modal dialog
*
* @ name Prompt . submitPasswordModal
* @ private
* @ function
* @ param { Event } event
* /
function submitPasswordModal ( event )
{
event . preventDefault ( ) ;
// get input
password = $passwordDecrypt . val ( ) ;
// hide modal
$passwordModal . modal ( 'hide' ) ;
PasteDecrypter . run ( ) ;
}
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* ask the user for the password and set it
2017-02-09 03:12:22 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Prompt . requestPassword
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . requestPassword = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-18 03:46:10 +08:00
// show new bootstrap method (if available)
if ( $passwordModal . length !== 0 ) {
$passwordModal . modal ( {
backdrop : 'static' ,
keyboard : false
} ) ;
return ;
}
2017-02-16 05:59:55 +08:00
2017-02-18 03:46:10 +08:00
// fallback to old method for page template
2018-07-21 14:44:04 +08:00
password = prompt ( I18n . _ ( 'Please enter the password for this paste:' ) , '' ) ;
if ( password === null ) {
2017-02-18 03:46:10 +08:00
throw 'password prompt canceled' ;
}
if ( password . length === 0 ) {
2017-11-20 15:49:25 +08:00
// recurse…
2017-02-18 03:46:10 +08:00
return me . requestPassword ( ) ;
}
2018-07-21 14:44:04 +08:00
PasteDecrypter . run ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
2017-04-11 22:34:13 +08:00
* get the cached password
2017-02-15 05:21:55 +08:00
*
2017-02-16 05:59:55 +08:00
* If you do not get a password with this function
* ( returns an empty string ) , use requestPassword .
2017-02-15 05:21:55 +08:00
*
* @ name Prompt . getPassword
* @ function
2017-02-16 05:59:55 +08:00
* @ return { string }
2017-02-15 05:21:55 +08:00
* /
me . getPassword = function ( )
{
return password ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-06 05:09:46 +08:00
2017-04-12 04:21:30 +08:00
/ * *
* resets the password to an empty string
*
* @ name Prompt . reset
* @ function
* /
me . reset = function ( )
{
// reset internal
password = '' ;
// and also reset UI
$passwordDecrypt . val ( '' ) ;
}
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* init status manager
2017-02-09 03:12:22 +08:00
*
2017-02-13 18:35:04 +08:00
* preloads jQuery elements
*
2017-03-14 03:24:18 +08:00
* @ name Prompt . init
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . init = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
$passwordDecrypt = $ ( '#passworddecrypt' ) ;
2017-02-18 05:46:18 +08:00
$passwordForm = $ ( '#passwordform' ) ;
$passwordModal = $ ( '#passwordmodal' ) ;
2017-02-09 03:12:22 +08:00
2017-02-13 18:35:04 +08:00
// bind events
2017-02-09 03:12:22 +08:00
2017-02-13 18:35:04 +08:00
// focus password input when it is shown
2017-02-18 03:46:10 +08:00
$passwordModal . on ( 'shown.bs.Model' , function ( ) {
2017-02-13 18:35:04 +08:00
$passwordDecrypt . focus ( ) ;
} ) ;
2017-02-16 05:59:55 +08:00
// handle Model password submission
2017-02-18 03:46:10 +08:00
$passwordForm . submit ( submitPasswordModal ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2017-02-13 01:08:08 +08:00
return me ;
2017-03-25 07:58:59 +08:00
} ) ( ) ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* Manage paste / message input , and preview tab
*
* Note that the actual preview is handled by PasteViewer .
2017-02-13 01:08:08 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name Editor
2017-02-13 01:08:08 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const Editor = ( function ( ) {
const me = { } ;
2017-02-13 01:08:08 +08:00
2018-12-30 01:40:59 +08:00
let $editorTabs ,
2017-02-13 18:35:04 +08:00
$messageEdit ,
$messagePreview ,
2018-12-30 01:40:59 +08:00
$message ,
isPreview = false ;
2016-08-09 20:46:32 +08:00
2015-10-16 04:06:01 +08:00
/ * *
2017-01-14 22:29:12 +08:00
* support input of tab character
2015-10-16 04:06:01 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . supportTabs
2017-01-14 22:29:12 +08:00
* @ function
* @ param { Event } event
2017-02-16 05:59:55 +08:00
* @ this $message ( but not used , so it is jQuery - free , possibly faster )
2015-10-16 04:06:01 +08:00
* /
2017-02-13 18:35:04 +08:00
function supportTabs ( event )
2015-10-16 04:06:01 +08:00
{
2018-12-30 01:40:59 +08:00
const keyCode = event . keyCode || event . which ;
2015-10-16 04:06:01 +08:00
// tab was pressed
2017-02-16 05:59:55 +08:00
if ( keyCode === 9 ) {
2015-10-16 04:06:01 +08:00
// get caret position & selection
2018-12-30 01:40:59 +08:00
const val = this . value ,
start = this . selectionStart ,
end = this . selectionEnd ;
2015-10-16 04:06:01 +08:00
// set textarea value to: text before caret + tab + text after caret
this . value = val . substring ( 0 , start ) + '\t' + val . substring ( end ) ;
// put caret at right position again
this . selectionStart = this . selectionEnd = start + 1 ;
2017-02-16 05:59:55 +08:00
// prevent the textarea to lose focus
event . preventDefault ( ) ;
2015-10-16 04:06:01 +08:00
}
2017-02-13 18:35:04 +08:00
}
2015-10-16 04:06:01 +08:00
2016-07-11 17:09:41 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* view the Editor tab
2016-07-11 17:09:41 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . viewEditor
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 18:35:04 +08:00
* @ param { Event } event - optional
2016-07-11 17:09:41 +08:00
* /
2017-02-13 18:35:04 +08:00
function viewEditor ( event )
2016-07-11 17:09:41 +08:00
{
2017-02-13 18:35:04 +08:00
// toggle buttons
$messageEdit . addClass ( 'active' ) ;
$messagePreview . removeClass ( 'active' ) ;
2020-01-27 01:08:59 +08:00
$ ( '#messageedit' ) . attr ( 'aria-selected' , 'true' ) ;
$ ( '#messagepreview' ) . attr ( 'aria-selected' , 'false' ) ;
2020-01-26 01:47:18 +08:00
2017-02-15 05:21:55 +08:00
PasteViewer . hide ( ) ;
2017-02-13 18:35:04 +08:00
// reshow input
$message . removeClass ( 'hidden' ) ;
me . focusInput ( ) ;
// finish
isPreview = false ;
2017-02-15 05:21:55 +08:00
// prevent jumping of page to top
if ( typeof event !== 'undefined' ) {
event . preventDefault ( ) ;
}
2017-02-13 18:35:04 +08:00
}
2016-07-11 17:09:41 +08:00
/ * *
2017-01-14 22:29:12 +08:00
* view the preview tab
2016-07-11 17:09:41 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . viewPreview
2017-01-14 22:29:12 +08:00
* @ function
* @ param { Event } event
2016-07-11 17:09:41 +08:00
* /
2017-02-13 18:35:04 +08:00
function viewPreview ( event )
2016-07-11 17:09:41 +08:00
{
2017-02-13 18:35:04 +08:00
// toggle buttons
$messageEdit . removeClass ( 'active' ) ;
$messagePreview . addClass ( 'active' ) ;
2020-01-27 01:08:59 +08:00
$ ( '#messageedit' ) . attr ( 'aria-selected' , 'false' ) ;
$ ( '#messagepreview' ) . attr ( 'aria-selected' , 'true' ) ;
2020-01-26 01:47:18 +08:00
2017-02-13 18:35:04 +08:00
// hide input as now preview is shown
$message . addClass ( 'hidden' ) ;
// show preview
2017-02-15 05:21:55 +08:00
PasteViewer . setText ( $message . val ( ) ) ;
2017-05-16 04:05:52 +08:00
if ( AttachmentViewer . hasAttachmentData ( ) ) {
2019-06-15 15:35:26 +08:00
const attachment = AttachmentViewer . getAttachment ( ) ;
AttachmentViewer . handleBlobAttachmentPreview (
AttachmentViewer . getAttachmentPreview ( ) ,
attachment [ 0 ] , attachment [ 1 ]
) ;
2017-05-14 03:27:41 +08:00
}
2017-02-15 05:21:55 +08:00
PasteViewer . run ( ) ;
2017-02-13 18:35:04 +08:00
// finish
isPreview = true ;
2017-02-15 05:21:55 +08:00
// prevent jumping of page to top
if ( typeof event !== 'undefined' ) {
event . preventDefault ( ) ;
}
2017-02-13 18:35:04 +08:00
}
2016-07-11 17:09:41 +08:00
2017-02-05 21:47:03 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* get the state of the preview
2017-02-05 21:47:03 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . isPreview
2017-02-05 21:47:03 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . isPreview = function ( )
2017-02-05 21:47:03 +08:00
{
2017-02-13 18:35:04 +08:00
return isPreview ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-05 21:47:03 +08:00
2017-02-06 04:22:09 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* reset the Editor view
2017-02-06 04:22:09 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . resetInput
2017-02-06 04:22:09 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . resetInput = function ( )
2017-02-06 04:22:09 +08:00
{
2017-02-13 18:35:04 +08:00
// go back to input
if ( isPreview ) {
viewEditor ( ) ;
2017-02-06 04:22:09 +08:00
}
2017-02-13 18:35:04 +08:00
// clear content
$message . val ( '' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-06 04:22:09 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* shows the Editor
2017-01-14 22:29:12 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . show
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-13 18:35:04 +08:00
me . show = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 18:35:04 +08:00
$message . removeClass ( 'hidden' ) ;
$editorTabs . removeClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
2015-09-17 04:51:48 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* hides the Editor
2017-01-14 22:29:12 +08:00
*
2020-05-30 17:47:33 +08:00
* @ name Editor . hide
2017-01-14 22:29:12 +08:00
* @ function
2015-09-17 04:51:48 +08:00
* /
2017-02-13 18:35:04 +08:00
me . hide = function ( )
2015-09-17 04:51:48 +08:00
{
2017-02-13 18:35:04 +08:00
$message . addClass ( 'hidden' ) ;
$editorTabs . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-17 04:51:48 +08:00
2016-11-14 01:12:10 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* focuses the message input
2017-01-14 22:29:12 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . focusInput
2017-01-14 22:29:12 +08:00
* @ function
2016-11-14 01:12:10 +08:00
* /
2017-02-13 18:35:04 +08:00
me . focusInput = function ( )
2016-11-14 01:12:10 +08:00
{
2017-02-13 18:35:04 +08:00
$message . focus ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-11-14 01:12:10 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* sets a new text
2016-11-14 01:12:10 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name Editor . setText
2017-01-14 22:29:12 +08:00
* @ function
2017-02-16 05:59:55 +08:00
* @ param { string } newText
2016-11-14 01:12:10 +08:00
* /
2017-02-16 05:59:55 +08:00
me . setText = function ( newText )
2016-11-14 01:12:10 +08:00
{
2017-02-16 05:59:55 +08:00
$message . val ( newText ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-11-14 01:12:10 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* returns the current text
2015-09-05 23:12:11 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name Editor . getText
2017-01-14 22:29:12 +08:00
* @ function
2017-02-13 18:35:04 +08:00
* @ return { string }
2015-09-05 23:12:11 +08:00
* /
2017-02-13 18:35:04 +08:00
me . getText = function ( )
2015-09-05 23:12:11 +08:00
{
2018-01-06 17:57:54 +08:00
return $message . val ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 01:08:08 +08:00
* init status manager
2015-09-05 23:12:11 +08:00
*
2017-02-13 01:08:08 +08:00
* preloads jQuery elements
*
2017-02-15 05:21:55 +08:00
* @ name Editor . init
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-13 01:08:08 +08:00
me . init = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 18:35:04 +08:00
$editorTabs = $ ( '#editorTabs' ) ;
2017-02-18 05:46:18 +08:00
$message = $ ( '#message' ) ;
2017-02-13 01:08:08 +08:00
2017-02-13 18:35:04 +08:00
// bind events
$message . keydown ( supportTabs ) ;
2017-02-13 04:13:04 +08:00
2017-02-13 18:35:04 +08:00
// bind click events to tab switchers (a), but save parent of them
// (li)
$messageEdit = $ ( '#messageedit' ) . click ( viewEditor ) . parent ( ) ;
$messagePreview = $ ( '#messagepreview' ) . click ( viewPreview ) . parent ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2016-07-11 17:09:41 +08:00
2017-02-13 01:08:08 +08:00
return me ;
2017-03-25 07:58:59 +08:00
} ) ( ) ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* ( view ) Parse and show paste .
2017-02-13 01:08:08 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteViewer
2017-02-13 01:08:08 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const PasteViewer = ( function ( ) {
const me = { } ;
2017-02-13 01:08:08 +08:00
2018-12-30 01:40:59 +08:00
let $placeholder ,
2017-02-13 18:35:04 +08:00
$prettyMessage ,
2017-02-18 05:46:18 +08:00
$prettyPrint ,
2018-12-30 01:40:59 +08:00
$plainText ,
text ,
2017-02-13 18:35:04 +08:00
format = 'plaintext' ,
isDisplayed = false ,
isChanged = true ; // by default true as nothing was parsed yet
2012-04-23 22:30:02 +08:00
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* apply the set format on paste and displays it
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteViewer . parsePaste
2017-02-13 18:35:04 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-13 18:35:04 +08:00
function parsePaste ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 18:35:04 +08:00
// skip parsing if no text is given
if ( text === '' ) {
return ;
2017-02-13 01:08:08 +08:00
}
2016-08-09 20:46:32 +08:00
2020-02-29 15:45:56 +08:00
if ( format === 'markdown' ) {
const converter = new showdown . Converter ( {
strikethrough : true ,
tables : true ,
tablesHeaderId : true ,
simplifiedAutoLink : true ,
excludeTrailingPunctuationFromURLs : true
} ) ;
// let showdown convert the HTML and sanitize HTML *afterwards*!
$plainText . html (
DOMPurify . sanitize (
converter . makeHtml ( text )
)
) ;
// add table classes from bootstrap css
$plainText . find ( 'table' ) . addClass ( 'table-condensed table-bordered' ) ;
} else {
if ( format === 'syntaxhighlighting' ) {
2017-11-23 05:27:38 +08:00
// yes, this is really needed to initialize the environment
2017-02-13 18:35:04 +08:00
if ( typeof prettyPrint === 'function' )
{
prettyPrint ( ) ;
}
2016-11-14 01:12:10 +08:00
2020-03-07 05:18:38 +08:00
$prettyPrint . html (
prettyPrintOne (
Helper . htmlEntities ( text ) , null , true
)
2017-02-13 18:35:04 +08:00
) ;
2020-02-29 15:45:56 +08:00
} else {
// = 'plaintext'
2020-03-07 05:18:38 +08:00
$prettyPrint . text ( text ) ;
2020-02-29 15:45:56 +08:00
}
2020-03-07 05:18:38 +08:00
Helper . urls2links ( $prettyPrint ) ;
2020-02-29 15:45:56 +08:00
$prettyPrint . css ( 'white-space' , 'pre-wrap' ) ;
$prettyPrint . css ( 'word-break' , 'normal' ) ;
$prettyPrint . removeClass ( 'prettyprint' ) ;
2017-02-13 18:35:04 +08:00
}
}
2015-09-05 23:12:11 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* displays the paste
2017-01-14 22:29:12 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteViewer . showPaste
2017-02-13 18:35:04 +08:00
* @ private
2017-01-14 22:29:12 +08:00
* @ function
2015-09-05 23:12:11 +08:00
* /
2017-02-13 18:35:04 +08:00
function showPaste ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 18:35:04 +08:00
// instead of "nothing" better display a placeholder
if ( text === '' ) {
2018-01-06 16:26:10 +08:00
$placeholder . removeClass ( 'hidden' ) ;
2015-09-05 23:12:11 +08:00
return ;
}
2017-02-13 18:35:04 +08:00
// otherwise hide the placeholder
2018-01-06 16:26:10 +08:00
$placeholder . addClass ( 'hidden' ) ;
2012-04-23 22:30:02 +08:00
2017-02-13 18:35:04 +08:00
switch ( format ) {
case 'markdown' :
2017-02-15 05:21:55 +08:00
$plainText . removeClass ( 'hidden' ) ;
2017-02-13 18:35:04 +08:00
$prettyMessage . addClass ( 'hidden' ) ;
break ;
default :
2017-02-15 05:21:55 +08:00
$plainText . addClass ( 'hidden' ) ;
2017-02-13 18:35:04 +08:00
$prettyMessage . removeClass ( 'hidden' ) ;
break ;
2015-09-05 23:12:11 +08:00
}
2017-02-13 18:35:04 +08:00
}
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* sets the format in which the text is shown
2017-02-09 03:12:22 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name PasteViewer . setFormat
2017-02-09 03:12:22 +08:00
* @ function
2017-11-21 17:53:33 +08:00
* @ param { string } newFormat the new format
2017-02-09 03:12:22 +08:00
* /
2017-02-13 18:35:04 +08:00
me . setFormat = function ( newFormat )
2017-02-09 03:12:22 +08:00
{
2017-02-16 05:59:55 +08:00
// skip if there is no update
if ( format === newFormat ) {
return ;
2015-09-05 23:12:11 +08:00
}
2017-02-16 05:59:55 +08:00
2017-11-21 17:53:33 +08:00
// needs to update display too, if we switch from or to Markdown
2017-02-16 05:59:55 +08:00
if ( format === 'markdown' || newFormat === 'markdown' ) {
isDisplayed = false ;
2015-09-05 23:12:11 +08:00
}
2017-02-16 05:59:55 +08:00
format = newFormat ;
isChanged = true ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* returns the current format
2017-02-09 03:12:22 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteViewer . getFormat
2017-02-09 03:12:22 +08:00
* @ function
2017-02-13 18:35:04 +08:00
* @ return { string }
2017-02-09 03:12:22 +08:00
* /
2017-02-13 18:35:04 +08:00
me . getFormat = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
return format ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-15 05:21:55 +08:00
/ * *
* returns whether the current view is pretty printed
*
* @ name PasteViewer . isPrettyPrinted
* @ function
* @ return { bool }
* /
me . isPrettyPrinted = function ( )
{
return $prettyPrint . hasClass ( 'prettyprinted' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* sets the text to show
2017-02-09 03:12:22 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name PasteViewer . setText
2017-02-09 03:12:22 +08:00
* @ function
2017-02-15 05:21:55 +08:00
* @ param { string } newText the text to show
2017-02-09 03:12:22 +08:00
* /
2017-02-13 18:35:04 +08:00
me . setText = function ( newText )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
if ( text !== newText ) {
text = newText ;
isChanged = true ;
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* gets the current cached text
*
* @ name PasteViewer . getText
* @ function
* @ return { string }
* /
2017-03-25 07:58:59 +08:00
me . getText = function ( )
2017-02-16 05:59:55 +08:00
{
return text ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-16 05:59:55 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* show / update the parsed text ( preview )
2017-02-09 03:12:22 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name PasteViewer . run
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-15 05:21:55 +08:00
me . run = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
if ( isChanged ) {
parsePaste ( ) ;
isChanged = false ;
}
if ( ! isDisplayed ) {
showPaste ( ) ;
isDisplayed = true ;
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* hide parsed text ( preview )
2017-02-09 03:12:22 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name PasteViewer . hide
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
me . hide = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
if ( ! isDisplayed ) {
2018-12-26 00:34:39 +08:00
return ;
2017-02-13 18:35:04 +08:00
}
2017-02-15 05:21:55 +08:00
$plainText . addClass ( 'hidden' ) ;
2017-02-13 18:35:04 +08:00
$prettyMessage . addClass ( 'hidden' ) ;
$placeholder . addClass ( 'hidden' ) ;
2017-04-03 00:58:11 +08:00
AttachmentViewer . hideAttachmentPreview ( ) ;
2017-02-13 18:35:04 +08:00
isDisplayed = false ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 01:08:08 +08:00
* init status manager
2017-02-09 03:12:22 +08:00
*
2017-02-13 01:08:08 +08:00
* preloads jQuery elements
*
2017-03-14 03:24:18 +08:00
* @ name PasteViewer . init
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 01:08:08 +08:00
me . init = function ( )
2017-02-09 03:12:22 +08:00
{
2017-02-13 18:35:04 +08:00
$placeholder = $ ( '#placeholder' ) ;
2017-02-18 05:46:18 +08:00
$plainText = $ ( '#plaintext' ) ;
2017-02-13 18:35:04 +08:00
$prettyMessage = $ ( '#prettymessage' ) ;
$prettyPrint = $ ( '#prettyprint' ) ;
2017-02-13 01:08:08 +08:00
2017-02-13 18:35:04 +08:00
// get default option from template/HTML or fall back to set value
2017-02-16 05:59:55 +08:00
format = Model . getFormatDefault ( ) || format ;
2017-11-21 17:53:33 +08:00
text = '' ;
isDisplayed = false ;
isChanged = true ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
return me ;
2017-03-25 07:58:59 +08:00
} ) ( ) ;
2017-02-15 05:21:55 +08:00
/ * *
* ( view ) Show attachment and preview if possible
*
2017-03-14 03:24:18 +08:00
* @ name AttachmentViewer
2017-02-15 05:21:55 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const AttachmentViewer = ( function ( ) {
const me = { } ;
2017-02-15 05:21:55 +08:00
2018-12-30 01:40:59 +08:00
let $attachmentLink ,
$attachmentPreview ,
$attachment ,
attachmentData ,
file ,
$fileInput ,
$dragAndDropFileName ,
2019-08-16 07:28:42 +08:00
attachmentHasPreview = false ,
$dropzone ;
2017-02-15 05:21:55 +08:00
/ * *
* sets the attachment but does not yet show it
*
* @ name AttachmentViewer . setAttachment
* @ function
* @ param { string } attachmentData - base64 - encoded data of file
* @ param { string } fileName - optional , file name
* /
me . setAttachment = function ( attachmentData , fileName )
{
2019-06-12 09:37:17 +08:00
// data URI format: data:[<mediaType>][;base64],<data>
// position in data URI string of where data begins
const base64Start = attachmentData . indexOf ( ',' ) + 1 ;
// position in data URI string of where mediaType ends
const mediaTypeEnd = attachmentData . indexOf ( ';' ) ;
// extract mediaType
const mediaType = attachmentData . substring ( 5 , mediaTypeEnd ) ;
// extract data and convert to binary
2020-07-01 02:10:56 +08:00
const rawData = attachmentData . substring ( base64Start ) ;
const decodedData = rawData . length > 0 ? atob ( rawData ) : '' ;
2019-06-12 09:37:17 +08:00
// Transform into a Blob
const buf = new Uint8Array ( decodedData . length ) ;
for ( let i = 0 ; i < decodedData . length ; ++ i ) {
buf [ i ] = decodedData . charCodeAt ( i ) ;
}
const blob = new window . Blob ( [ buf ] , { type : mediaType } ) ;
2019-06-12 11:29:36 +08:00
// Get Blob URL
const blobUrl = window . URL . createObjectURL ( blob ) ;
2018-04-09 06:36:55 +08:00
// IE does not support setting a data URI on an a element
2019-06-12 09:37:17 +08:00
// Using msSaveBlob to download
2018-04-09 06:36:55 +08:00
if ( window . Blob && navigator . msSaveBlob ) {
2018-04-09 23:57:58 +08:00
$attachmentLink . off ( 'click' ) . on ( 'click' , function ( ) {
2018-04-09 12:44:37 +08:00
navigator . msSaveBlob ( blob , fileName ) ;
2018-04-09 06:36:55 +08:00
} ) ;
2018-04-07 14:29:36 +08:00
} else {
2019-06-12 11:29:36 +08:00
$attachmentLink . attr ( 'href' , blobUrl ) ;
2018-04-07 14:29:36 +08:00
}
2017-02-15 05:21:55 +08:00
if ( typeof fileName !== 'undefined' ) {
$attachmentLink . attr ( 'download' , fileName ) ;
}
2019-06-12 10:29:19 +08:00
me . handleBlobAttachmentPreview ( $attachmentPreview , blobUrl , mediaType ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
* displays the attachment
*
* @ name AttachmentViewer . showAttachment
* @ function
* /
me . showAttachment = function ( )
{
$attachment . removeClass ( 'hidden' ) ;
if ( attachmentHasPreview ) {
$attachmentPreview . removeClass ( 'hidden' ) ;
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* removes the attachment
*
2017-08-12 19:26:43 +08:00
* This automatically hides the attachment containers too , to
2017-02-16 05:59:55 +08:00
* prevent an inconsistent display .
2017-02-15 05:21:55 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name AttachmentViewer . removeAttachment
2017-02-15 05:21:55 +08:00
* @ function
* /
me . removeAttachment = function ( )
{
2017-05-16 04:05:52 +08:00
if ( ! $attachment . length ) {
2017-05-14 03:27:41 +08:00
return ;
}
2017-02-16 05:59:55 +08:00
me . hideAttachment ( ) ;
me . hideAttachmentPreview ( ) ;
2017-08-12 19:26:43 +08:00
$attachmentLink . removeAttr ( 'href' ) ;
$attachmentLink . removeAttr ( 'download' ) ;
2018-04-09 23:57:58 +08:00
$attachmentLink . off ( 'click' ) ;
2017-02-16 05:59:55 +08:00
$attachmentPreview . html ( '' ) ;
2019-08-18 10:17:35 +08:00
$dragAndDropFileName . text ( '' ) ;
2017-04-03 00:58:11 +08:00
2018-05-22 01:32:01 +08:00
AttachmentViewer . removeAttachmentData ( ) ;
} ;
/ * *
* removes the attachment data
*
* This removes the data , which would be uploaded otherwise .
*
* @ name AttachmentViewer . removeAttachmentData
* @ function
* /
me . removeAttachmentData = function ( )
{
2017-05-16 04:05:52 +08:00
file = undefined ;
attachmentData = undefined ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2018-05-22 06:41:02 +08:00
/ * *
* Cleares the drag & drop data .
*
* @ name AttachmentViewer . clearDragAndDrop
* @ function
* /
me . clearDragAndDrop = function ( )
{
$dragAndDropFileName . text ( '' ) ;
} ;
2017-02-16 05:59:55 +08:00
/ * *
* hides the attachment
*
2017-03-14 03:24:18 +08:00
* This will not hide the preview ( see AttachmentViewer . hideAttachmentPreview
* for that ) nor will it hide the attachment link if it was moved somewhere
* else ( see AttachmentViewer . moveAttachmentTo ) .
2017-02-16 05:59:55 +08:00
*
* @ name AttachmentViewer . hideAttachment
* @ function
* /
me . hideAttachment = function ( )
{
$attachment . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* hides the attachment preview
*
* @ name AttachmentViewer . hideAttachmentPreview
* @ function
* /
me . hideAttachmentPreview = function ( )
{
2017-05-16 04:05:52 +08:00
if ( $attachmentPreview ) {
$attachmentPreview . addClass ( 'hidden' ) ;
2017-05-14 03:27:41 +08:00
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
2018-05-22 01:32:01 +08:00
* checks if there is an attachment displayed
2017-02-15 05:21:55 +08:00
*
* @ name AttachmentViewer . hasAttachment
* @ function
* /
me . hasAttachment = function ( )
{
2017-05-16 04:05:52 +08:00
if ( ! $attachment . length ) {
2017-05-14 03:27:41 +08:00
return false ;
}
2018-12-30 01:40:59 +08:00
const link = $attachmentLink . prop ( 'href' ) ;
2017-05-16 04:05:52 +08:00
return ( typeof link !== 'undefined' && link !== '' ) ;
} ;
/ * *
2018-05-22 01:32:01 +08:00
* checks if there is attachment data ( for preview ! ) available
*
* It returns true , when there is data that needs to be encrypted .
2017-05-16 04:05:52 +08:00
*
* @ name AttachmentViewer . hasAttachmentData
* @ function
* /
me . hasAttachmentData = function ( )
{
if ( $attachment . length ) {
return true ;
}
return false ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
* return the attachment
*
* @ name AttachmentViewer . getAttachment
* @ function
* @ returns { array }
* /
me . getAttachment = function ( )
{
return [
2017-02-16 05:59:55 +08:00
$attachmentLink . prop ( 'href' ) ,
$attachmentLink . prop ( 'download' )
2017-02-15 05:21:55 +08:00
] ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* moves the attachment link to another element
*
* It is advisable to hide the attachment afterwards ( AttachmentViewer . hideAttachment )
*
2017-03-14 03:24:18 +08:00
* @ name AttachmentViewer . moveAttachmentTo
2017-02-16 05:59:55 +08:00
* @ function
* @ param { jQuery } $element - the wrapper / container element where this should be moved to
* @ param { string } label - the text to show ( % s will be replaced with the file name ) , will automatically be translated
* /
me . moveAttachmentTo = function ( $element , label )
{
// move elemement to new place
$attachmentLink . appendTo ( $element ) ;
2019-12-25 16:14:32 +08:00
// update text - ensuring no HTML is inserted into the text node
2020-01-04 18:34:16 +08:00
I18n . _ ( $attachmentLink , label , $attachmentLink . attr ( 'download' ) ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-16 05:59:55 +08:00
2017-02-15 05:21:55 +08:00
/ * *
2018-07-22 16:24:39 +08:00
* read file data as data URL using the FileReader API
2017-02-15 05:21:55 +08:00
*
2018-04-29 17:57:03 +08:00
* @ name AttachmentViewer . readFileData
2018-05-22 06:43:24 +08:00
* @ private
2017-02-15 05:21:55 +08:00
* @ function
2018-07-22 16:24:39 +08:00
* @ param { object } loadedFile ( optional ) loaded file object
2018-04-29 17:57:03 +08:00
* @ see { @ link https : //developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL()}
2017-02-15 05:21:55 +08:00
* /
2018-05-22 16:19:53 +08:00
function readFileData ( loadedFile ) {
2017-05-14 01:46:22 +08:00
if ( typeof FileReader === 'undefined' ) {
// revert loading status…
2017-05-16 04:05:52 +08:00
me . hideAttachment ( ) ;
me . hideAttachmentPreview ( ) ;
2019-08-27 13:38:27 +08:00
Alert . showWarning ( 'Your browser does not support uploading encrypted files. Please use a newer browser.' ) ;
2017-05-14 01:46:22 +08:00
return ;
}
2017-02-15 05:21:55 +08:00
2018-12-30 01:40:59 +08:00
const fileReader = new FileReader ( ) ;
2017-05-20 22:04:10 +08:00
if ( loadedFile === undefined ) {
loadedFile = $fileInput [ 0 ] . files [ 0 ] ;
$dragAndDropFileName . text ( '' ) ;
2017-05-14 01:46:22 +08:00
} else {
2017-05-20 22:04:10 +08:00
$dragAndDropFileName . text ( loadedFile . name ) ;
2017-05-14 01:46:22 +08:00
}
2017-02-15 05:21:55 +08:00
2019-08-16 07:28:42 +08:00
if ( typeof loadedFile !== 'undefined' ) {
2019-08-17 01:38:08 +08:00
file = loadedFile ;
2019-08-16 07:28:42 +08:00
fileReader . onload = function ( event ) {
const dataURL = event . target . result ;
attachmentData = dataURL ;
2017-05-14 01:46:22 +08:00
2019-08-16 07:28:42 +08:00
if ( Editor . isPreview ( ) ) {
me . handleAttachmentPreview ( $attachmentPreview , dataURL ) ;
$attachmentPreview . removeClass ( 'hidden' ) ;
}
TopNav . highlightFileupload ( ) ;
} ;
fileReader . readAsDataURL ( loadedFile ) ;
2019-08-17 01:38:08 +08:00
} else {
me . removeAttachmentData ( ) ;
2019-08-16 07:28:42 +08:00
}
2018-05-22 16:19:53 +08:00
}
2017-05-14 01:46:22 +08:00
2019-06-12 10:29:19 +08:00
/ * *
* handle the preview of files decoded to blob that can either be an image , video , audio or pdf element
*
* @ name AttachmentViewer . handleBlobAttachmentPreview
* @ function
* @ argument { jQuery } $targetElement element where the preview should be appended
* @ argument { string } file as a blob URL
* @ argument { string } mime type
* /
me . handleBlobAttachmentPreview = function ( $targetElement , blobUrl , mimeType ) {
if ( blobUrl ) {
attachmentHasPreview = true ;
if ( mimeType . match ( /image\//i ) ) {
$targetElement . html (
$ ( document . createElement ( 'img' ) )
. attr ( 'src' , blobUrl )
. attr ( 'class' , 'img-thumbnail' )
) ;
} else if ( mimeType . match ( /video\//i ) ) {
$targetElement . html (
$ ( document . createElement ( 'video' ) )
. attr ( 'controls' , 'true' )
. attr ( 'autoplay' , 'true' )
. attr ( 'class' , 'img-thumbnail' )
. append ( $ ( document . createElement ( 'source' ) )
. attr ( 'type' , mimeType )
. attr ( 'src' , blobUrl ) )
) ;
} else if ( mimeType . match ( /audio\//i ) ) {
$targetElement . html (
$ ( document . createElement ( 'audio' ) )
. attr ( 'controls' , 'true' )
. attr ( 'autoplay' , 'true' )
. append ( $ ( document . createElement ( 'source' ) )
. attr ( 'type' , mimeType )
. attr ( 'src' , blobUrl ) )
) ;
} else if ( mimeType . match ( /\/pdf/i ) ) {
// Fallback for browsers, that don't support the vh unit
2019-08-16 07:28:42 +08:00
const clientHeight = $ ( window ) . height ( ) ;
2019-06-12 10:29:19 +08:00
$targetElement . html (
$ ( document . createElement ( 'embed' ) )
. attr ( 'src' , blobUrl )
. attr ( 'type' , 'application/pdf' )
. attr ( 'class' , 'pdfPreview' )
. css ( 'height' , clientHeight )
) ;
} else {
attachmentHasPreview = false ;
}
}
} ;
2017-05-14 01:46:22 +08:00
/ * *
2018-04-29 17:57:03 +08:00
* attaches the file attachment drag & drop handler to the page
*
* @ name AttachmentViewer . addDragDropHandler
2018-05-22 06:43:24 +08:00
* @ private
2018-04-29 17:57:03 +08:00
* @ function
2017-05-14 01:46:22 +08:00
* /
2018-05-22 16:19:53 +08:00
function addDragDropHandler ( ) {
2017-05-16 04:05:52 +08:00
if ( typeof $fileInput === 'undefined' || $fileInput . length === 0 ) {
2017-05-14 01:46:22 +08:00
return ;
}
2019-08-16 07:28:42 +08:00
const handleDragEnterOrOver = function ( event ) {
2017-05-14 01:46:22 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2019-08-18 10:17:35 +08:00
return false ;
2017-05-14 01:46:22 +08:00
} ;
2019-08-16 07:28:42 +08:00
const handleDrop = function ( event ) {
2018-12-30 01:40:59 +08:00
const evt = event . originalEvent ;
2017-05-16 04:05:52 +08:00
evt . stopPropagation ( ) ;
evt . preventDefault ( ) ;
2017-05-14 01:46:22 +08:00
2019-08-26 00:00:59 +08:00
if ( TopNav . isAttachmentReadonly ( ) ) {
2019-08-18 10:17:35 +08:00
return false ;
}
2017-05-16 04:05:52 +08:00
if ( $fileInput ) {
2018-12-30 01:40:59 +08:00
const file = evt . dataTransfer . files [ 0 ] ;
2017-05-14 01:46:22 +08:00
//Clear the file input:
2017-05-16 04:05:52 +08:00
$fileInput . wrap ( '<form>' ) . closest ( 'form' ) . get ( 0 ) . reset ( ) ;
$fileInput . unwrap ( ) ;
2017-05-14 01:46:22 +08:00
//Only works in Chrome:
//fileInput[0].files = e.dataTransfer.files;
2018-05-22 06:43:24 +08:00
readFileData ( file ) ;
2017-05-14 01:46:22 +08:00
}
} ;
2019-08-16 07:28:42 +08:00
$ ( document ) . draghover ( ) . on ( {
2019-08-18 10:17:35 +08:00
'draghoverstart' : function ( e ) {
2019-08-26 00:00:59 +08:00
if ( TopNav . isAttachmentReadonly ( ) ) {
2019-08-18 10:17:35 +08:00
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
return false ;
}
2019-08-16 07:28:42 +08:00
// show dropzone to indicate drop support
$dropzone . removeClass ( 'hidden' ) ;
} ,
'draghoverend' : function ( ) {
$dropzone . addClass ( 'hidden' ) ;
}
} ) ;
$ ( document ) . on ( 'drop' , handleDrop ) ;
$ ( document ) . on ( 'dragenter dragover' , handleDragEnterOrOver ) ;
2018-05-22 06:41:02 +08:00
$fileInput . on ( 'change' , function ( ) {
2018-05-22 06:43:24 +08:00
readFileData ( ) ;
2017-05-14 01:46:22 +08:00
} ) ;
2018-05-22 16:19:53 +08:00
}
2017-05-14 01:46:22 +08:00
2017-05-14 03:43:32 +08:00
/ * *
2018-04-29 17:57:03 +08:00
* attaches the clipboard attachment handler to the page
*
* @ name AttachmentViewer . addClipboardEventHandler
2018-05-22 06:43:24 +08:00
* @ private
2018-04-29 17:57:03 +08:00
* @ function
2017-05-14 03:43:32 +08:00
* /
2018-05-22 16:19:53 +08:00
function addClipboardEventHandler ( ) {
$ ( document ) . on ( 'paste' , function ( event ) {
2018-12-30 01:40:59 +08:00
const items = ( event . clipboardData || event . originalEvent . clipboardData ) . items ;
2020-05-17 13:35:19 +08:00
const lastItem = items [ items . length - 1 ] ;
if ( lastItem . kind === 'file' ) {
if ( TopNav . isAttachmentReadonly ( ) ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return false ;
} else {
readFileData ( lastItem . getAsFile ( ) ) ;
2018-05-22 16:19:53 +08:00
}
}
} ) ;
}
2017-05-16 04:05:52 +08:00
2018-04-29 17:57:03 +08:00
/ * *
* getter for attachment data
*
* @ name AttachmentViewer . getAttachmentData
* @ function
* @ return { jQuery }
* /
2017-05-16 04:05:52 +08:00
me . getAttachmentData = function ( ) {
return attachmentData ;
} ;
2018-04-29 17:57:03 +08:00
/ * *
* getter for attachment link
*
* @ name AttachmentViewer . getAttachmentLink
* @ function
* @ return { jQuery }
* /
2017-05-16 04:05:52 +08:00
me . getAttachmentLink = function ( ) {
return $attachmentLink ;
} ;
2018-04-29 17:57:03 +08:00
/ * *
* getter for attachment preview
*
* @ name AttachmentViewer . getAttachmentPreview
* @ function
* @ return { jQuery }
* /
2017-05-16 04:05:52 +08:00
me . getAttachmentPreview = function ( ) {
return $attachmentPreview ;
} ;
2018-04-29 17:57:03 +08:00
/ * *
* getter for file data , returns the file contents
*
* @ name AttachmentViewer . getFile
* @ function
* @ return { string }
* /
2017-05-16 04:05:52 +08:00
me . getFile = function ( ) {
return file ;
2017-05-14 03:43:32 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
* initiate
*
* preloads jQuery elements
*
* @ name AttachmentViewer . init
* @ function
* /
me . init = function ( )
{
$attachment = $ ( '#attachment' ) ;
2019-08-29 02:29:23 +08:00
$dragAndDropFileName = $ ( '#dragAndDropFileName' ) ;
$dropzone = $ ( '#dropzone' ) ;
2020-01-04 20:33:03 +08:00
$attachmentLink = $ ( '#attachment a' ) || $ ( '<a>' ) ;
2019-08-29 02:29:23 +08:00
if ( $attachment . length ) {
2017-05-16 04:05:52 +08:00
$attachmentPreview = $ ( '#attachmentPreview' ) ;
2017-05-14 01:46:22 +08:00
2017-05-16 04:05:52 +08:00
$fileInput = $ ( '#file' ) ;
2018-05-22 06:43:24 +08:00
addDragDropHandler ( ) ;
addClipboardEventHandler ( ) ;
2017-05-14 03:27:41 +08:00
}
2017-02-25 16:35:55 +08:00
}
2017-02-15 05:21:55 +08:00
return me ;
2017-11-28 13:38:10 +08:00
} ) ( ) ;
2017-02-15 05:21:55 +08:00
/ * *
* ( view ) Shows discussion thread and handles replies
*
2017-03-14 03:24:18 +08:00
* @ name DiscussionViewer
2017-02-15 05:21:55 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const DiscussionViewer = ( function ( ) {
const me = { } ;
2017-02-15 05:21:55 +08:00
2018-12-30 01:40:59 +08:00
let $commentTail ,
2017-02-18 03:46:10 +08:00
$discussion ,
$reply ,
$replyMessage ,
$replyNickname ,
2017-02-18 05:46:18 +08:00
$replyStatus ,
2018-12-30 01:40:59 +08:00
$commentContainer ,
replyCommentId ;
2017-02-15 05:21:55 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* initializes the templates
2017-02-15 05:21:55 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name DiscussionViewer . initTemplates
2017-02-18 03:46:10 +08:00
* @ private
2017-02-15 05:21:55 +08:00
* @ function
* /
2017-02-18 03:46:10 +08:00
function initTemplates ( )
2017-02-15 05:21:55 +08:00
{
2017-02-18 03:46:10 +08:00
$reply = Model . getTemplate ( 'reply' ) ;
$replyMessage = $reply . find ( '#replymessage' ) ;
$replyNickname = $reply . find ( '#nickname' ) ;
$replyStatus = $reply . find ( '#replystatus' ) ;
// cache jQuery elements
$commentTail = Model . getTemplate ( 'commenttail' ) ;
}
2017-02-15 05:21:55 +08:00
2017-03-14 03:24:18 +08:00
/ * *
* open the comment entry when clicking the "Reply" button of a comment
*
* @ name DiscussionViewer . openReply
* @ private
* @ function
* @ param { Event } event
* /
function openReply ( event )
{
2018-12-30 01:40:59 +08:00
const $source = $ ( event . target ) ;
2017-03-14 03:24:18 +08:00
// clear input
$replyMessage . val ( '' ) ;
$replyNickname . val ( '' ) ;
// get comment id from source element
replyCommentId = $source . parent ( ) . prop ( 'id' ) . split ( '_' ) [ 1 ] ;
// move to correct position
$source . after ( $reply ) ;
// show
$reply . removeClass ( 'hidden' ) ;
$replyMessage . focus ( ) ;
event . preventDefault ( ) ;
}
2017-02-15 05:21:55 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* custom handler for displaying notifications in own status message area
2017-02-15 05:21:55 +08:00
*
2017-02-18 03:46:10 +08:00
* @ name DiscussionViewer . handleNotification
2017-02-15 05:21:55 +08:00
* @ function
2017-02-18 03:46:10 +08:00
* @ param { string } alertType
* @ return { bool | jQuery }
2017-02-15 05:21:55 +08:00
* /
2018-01-06 20:32:07 +08:00
me . handleNotification = function ( alertType )
2017-02-15 05:21:55 +08:00
{
2017-02-18 03:46:10 +08:00
// ignore loading messages
if ( alertType === 'loading' ) {
return false ;
}
2017-02-15 05:21:55 +08:00
2017-02-18 03:46:10 +08:00
if ( alertType === 'danger' ) {
$replyStatus . removeClass ( 'alert-info' ) ;
$replyStatus . addClass ( 'alert-danger' ) ;
$replyStatus . find ( ':first' ) . removeClass ( 'glyphicon-alert' ) ;
$replyStatus . find ( ':first' ) . addClass ( 'glyphicon-info-sign' ) ;
} else {
$replyStatus . removeClass ( 'alert-danger' ) ;
$replyStatus . addClass ( 'alert-info' ) ;
$replyStatus . find ( ':first' ) . removeClass ( 'glyphicon-info-sign' ) ;
$replyStatus . find ( ':first' ) . addClass ( 'glyphicon-alert' ) ;
}
return $replyStatus ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-02-18 03:46:10 +08:00
/ * *
* adds another comment
*
* @ name DiscussionViewer . addComment
* @ function
2019-05-25 19:20:39 +08:00
* @ param { Comment } comment
2017-02-18 03:46:10 +08:00
* @ param { string } commentText
2018-01-02 18:42:03 +08:00
* @ param { string } nickname
2017-02-18 03:46:10 +08:00
* /
2018-01-02 18:42:03 +08:00
me . addComment = function ( comment , commentText , nickname )
2017-02-18 03:46:10 +08:00
{
if ( commentText === '' ) {
commentText = 'comment decryption failed' ;
}
// create new comment based on template
2018-12-30 01:40:59 +08:00
const $commentEntry = Model . getTemplate ( 'comment' ) ;
2017-02-18 03:46:10 +08:00
$commentEntry . prop ( 'id' , 'comment_' + comment . id ) ;
2018-12-30 01:40:59 +08:00
const $commentEntryData = $commentEntry . find ( 'div.commentdata' ) ;
2017-02-18 03:46:10 +08:00
// set & parse text
2020-03-07 05:18:38 +08:00
$commentEntryData . text ( commentText ) ;
Helper . urls2links ( $commentEntryData ) ;
2017-02-18 03:46:10 +08:00
// set nickname
2017-03-13 00:08:12 +08:00
if ( nickname . length > 0 ) {
2017-02-18 03:46:10 +08:00
$commentEntry . find ( 'span.nickname' ) . text ( nickname ) ;
} else {
2017-03-13 00:08:12 +08:00
$commentEntry . find ( 'span.nickname' ) . html ( '<i></i>' ) ;
I18n . _ ( $commentEntry . find ( 'span.nickname i' ) , 'Anonymous' ) ;
2017-02-18 03:46:10 +08:00
}
// set date
$commentEntry . find ( 'span.commentdate' )
2019-05-25 19:20:39 +08:00
. text ( ' (' + ( new Date ( comment . getCreated ( ) * 1000 ) . toLocaleString ( ) ) + ')' )
2017-02-18 03:46:10 +08:00
. attr ( 'title' , 'CommentID: ' + comment . id ) ;
// if an avatar is available, display it
2019-05-25 19:20:39 +08:00
const icon = comment . getIcon ( ) ;
if ( icon ) {
2017-02-18 03:46:10 +08:00
$commentEntry . find ( 'span.nickname' )
2017-03-13 00:08:12 +08:00
. before (
2019-05-25 19:20:39 +08:00
'<img src="' + icon + '" class="vizhash" /> '
2017-03-13 00:08:12 +08:00
) ;
$ ( document ) . on ( 'languageLoaded' , function ( ) {
$commentEntry . find ( 'img.vizhash' )
. prop ( 'title' , I18n . _ ( 'Avatar generated from IP address' ) ) ;
} ) ;
2017-02-18 03:46:10 +08:00
}
2018-01-02 18:42:03 +08:00
// starting point (default value/fallback)
2018-12-30 01:40:59 +08:00
let $place = $commentContainer ;
2018-01-02 18:42:03 +08:00
// if parent comment exists
2018-12-30 01:40:59 +08:00
const $parentComment = $ ( '#comment_' + comment . parentid ) ;
2018-01-02 18:42:03 +08:00
if ( $parentComment . length ) {
// use parent as position for new comment, so it is shifted
// to the right
$place = $parentComment ;
}
2017-02-18 03:46:10 +08:00
// finally append comment
$place . append ( $commentEntry ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
/ * *
* finishes the discussion area after last comment
*
* @ name DiscussionViewer . finishDiscussion
* @ function
* /
me . finishDiscussion = function ( )
{
// add 'add new comment' area
$commentContainer . append ( $commentTail ) ;
// show discussions
$discussion . removeClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
/ * *
* removes the old discussion and prepares everything for creating a new
* one .
*
2017-12-18 21:47:17 +08:00
* @ name DiscussionViewer . prepareNewDiscussion
2017-02-18 03:46:10 +08:00
* @ function
* /
2017-12-18 21:47:17 +08:00
me . prepareNewDiscussion = function ( )
2017-02-18 03:46:10 +08:00
{
$commentContainer . html ( '' ) ;
$discussion . addClass ( 'hidden' ) ;
// (re-)init templates
initTemplates ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
/ * *
2017-12-18 21:47:17 +08:00
* returns the users message from the reply form
2017-02-18 03:46:10 +08:00
*
2017-12-18 21:47:17 +08:00
* @ name DiscussionViewer . getReplyMessage
2017-02-18 03:46:10 +08:00
* @ function
2017-12-18 21:47:17 +08:00
* @ return { String }
2017-02-18 03:46:10 +08:00
* /
2017-12-18 21:47:17 +08:00
me . getReplyMessage = function ( )
2017-02-18 03:46:10 +08:00
{
2017-12-18 21:47:17 +08:00
return $replyMessage . val ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-12-18 21:47:17 +08:00
/ * *
* returns the users nickname ( if any ) from the reply form
*
* @ name DiscussionViewer . getReplyNickname
* @ function
* @ return { String }
* /
me . getReplyNickname = function ( )
{
return $replyNickname . val ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-12-18 21:47:17 +08:00
/ * *
* returns the id of the parent comment the user is replying to
*
* @ name DiscussionViewer . getReplyCommentId
* @ function
* @ return { int | undefined }
* /
me . getReplyCommentId = function ( )
{
return replyCommentId ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
/ * *
* highlights a specific comment and scrolls to it if necessary
*
* @ name DiscussionViewer . highlightComment
* @ function
* @ param { string } commentId
* @ param { bool } fadeOut - whether to fade out the comment
* /
me . highlightComment = function ( commentId , fadeOut )
{
2018-12-30 01:40:59 +08:00
const $comment = $ ( '#comment_' + commentId ) ;
2017-02-18 03:46:10 +08:00
// in case comment does not exist, cancel
if ( $comment . length === 0 ) {
return ;
}
2018-07-01 14:59:55 +08:00
$comment . addClass ( 'highlight' ) ;
2018-12-30 01:40:59 +08:00
const highlightComment = function ( ) {
2017-02-18 03:46:10 +08:00
if ( fadeOut === true ) {
setTimeout ( function ( ) {
$comment . removeClass ( 'highlight' ) ;
2020-03-21 23:53:55 +08:00
2017-02-18 03:46:10 +08:00
} , 300 ) ;
}
2018-01-06 17:57:54 +08:00
} ;
2017-02-18 03:46:10 +08:00
if ( UiHelper . isVisible ( $comment ) ) {
return highlightComment ( ) ;
}
UiHelper . scrollTo ( $comment , 100 , 'swing' , highlightComment ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
* initiate
*
* preloads jQuery elements
*
2017-02-16 05:59:55 +08:00
* @ name DiscussionViewer . init
2017-02-15 05:21:55 +08:00
* @ function
* /
me . init = function ( )
{
2017-02-18 03:46:10 +08:00
// bind events to templates (so they are later cloned)
$ ( '#commenttailtemplate, #commenttemplate' ) . find ( 'button' ) . on ( 'click' , openReply ) ;
$ ( '#replytemplate' ) . find ( 'button' ) . on ( 'click' , PasteEncrypter . sendComment ) ;
$commentContainer = $ ( '#commentcontainer' ) ;
2017-02-15 05:21:55 +08:00
$discussion = $ ( '#discussion' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:11:04 +08:00
2017-02-13 01:08:08 +08:00
return me ;
2017-12-18 21:47:17 +08:00
} ) ( ) ;
2017-02-13 01:08:08 +08:00
/ * *
* Manage top ( navigation ) bar
*
2017-03-14 03:24:18 +08:00
* @ name TopNav
* @ param { object } window
* @ param { object } document
2017-02-13 01:08:08 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const TopNav = ( function ( window , document ) {
const me = { } ;
2017-02-13 04:13:04 +08:00
2018-12-30 01:40:59 +08:00
let createButtonsDisplayed = false ,
viewButtonsDisplayed = false ,
2021-04-05 19:24:53 +08:00
burnAfterReadingDefault = false ,
openDiscussionDefault = false ,
2018-12-30 01:40:59 +08:00
$attach ,
2017-02-13 01:08:08 +08:00
$burnAfterReading ,
$burnAfterReadingOption ,
$cloneButton ,
2017-02-16 05:59:55 +08:00
$customAttachment ,
2017-02-13 01:08:08 +08:00
$expiration ,
$fileRemoveButton ,
2017-02-18 05:59:16 +08:00
$fileWrap ,
2017-02-13 01:08:08 +08:00
$formatter ,
$newButton ,
$openDiscussion ,
2017-02-18 05:46:18 +08:00
$openDiscussionOption ,
2017-02-13 04:13:04 +08:00
$password ,
2017-02-14 04:12:00 +08:00
$passwordInput ,
2017-02-13 01:08:08 +08:00
$rawTextButton ,
2017-12-25 21:59:15 +08:00
$qrCodeLink ,
2019-08-22 05:36:22 +08:00
$emailLink ,
2017-04-12 04:21:30 +08:00
$sendButton ,
2018-12-30 01:40:59 +08:00
$retryButton ,
2019-08-22 05:36:22 +08:00
pasteExpiration = null ,
2017-04-12 04:21:30 +08:00
retryButtonCallback ;
2017-02-13 18:35:04 +08:00
2017-02-13 01:08:08 +08:00
/ * *
2017-02-13 18:35:04 +08:00
* set the expiration on bootstrap templates in dropdown
2017-02-13 01:08:08 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . updateExpiration
2017-03-14 03:24:18 +08:00
* @ private
2017-02-13 01:08:08 +08:00
* @ function
* @ param { Event } event
* /
2017-02-13 18:35:04 +08:00
function updateExpiration ( event )
2017-02-13 01:08:08 +08:00
{
2017-02-13 18:35:04 +08:00
// get selected option
2018-12-30 01:40:59 +08:00
const target = $ ( event . target ) ;
2017-02-13 18:35:04 +08:00
// update dropdown display and save new expiration time
2017-02-13 01:08:08 +08:00
$ ( '#pasteExpirationDisplay' ) . text ( target . text ( ) ) ;
2017-02-13 18:35:04 +08:00
pasteExpiration = target . data ( 'expiration' ) ;
event . preventDefault ( ) ;
2017-02-13 01:08:08 +08:00
}
/ * *
2019-08-18 10:17:35 +08:00
* set the format on bootstrap templates in dropdown from user interaction
2017-02-13 01:08:08 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . updateFormat
2017-03-14 03:24:18 +08:00
* @ private
2017-02-13 01:08:08 +08:00
* @ function
* @ param { Event } event
* /
2017-02-13 18:35:04 +08:00
function updateFormat ( event )
2017-02-13 01:08:08 +08:00
{
2017-02-13 18:35:04 +08:00
// get selected option
2018-12-30 01:40:59 +08:00
const $target = $ ( event . target ) ;
2017-02-13 01:08:08 +08:00
2017-02-13 18:35:04 +08:00
// update dropdown display and save new format
2018-12-30 01:40:59 +08:00
const newFormat = $target . data ( 'format' ) ;
2017-02-13 18:35:04 +08:00
$ ( '#pasteFormatterDisplay' ) . text ( $target . text ( ) ) ;
2017-02-15 05:21:55 +08:00
PasteViewer . setFormat ( newFormat ) ;
2017-02-13 18:35:04 +08:00
// update preview
2017-02-15 05:21:55 +08:00
if ( Editor . isPreview ( ) ) {
PasteViewer . run ( ) ;
2017-02-13 01:08:08 +08:00
}
2017-02-13 18:35:04 +08:00
2017-02-13 01:08:08 +08:00
event . preventDefault ( ) ;
2017-02-13 18:35:04 +08:00
}
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 01:08:08 +08:00
* when "burn after reading" is checked , disable discussion
2017-02-09 03:12:22 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . changeBurnAfterReading
2017-03-14 03:24:18 +08:00
* @ private
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 01:08:08 +08:00
function changeBurnAfterReading ( )
2017-02-09 03:11:04 +08:00
{
2017-02-13 18:35:04 +08:00
if ( $burnAfterReading . is ( ':checked' ) ) {
$openDiscussionOption . addClass ( 'buttondisabled' ) ;
$openDiscussion . prop ( 'checked' , false ) ;
// if button is actually disabled, force-enable it and uncheck other button
$burnAfterReadingOption . removeClass ( 'buttondisabled' ) ;
} else {
$openDiscussionOption . removeClass ( 'buttondisabled' ) ;
2017-02-09 03:12:22 +08:00
}
2017-02-13 01:08:08 +08:00
}
2015-09-05 23:12:11 +08:00
2017-02-09 03:12:22 +08:00
/ * *
2017-02-13 01:08:08 +08:00
* when discussion is checked , disable "burn after reading"
2017-02-09 03:12:22 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . changeOpenDiscussion
2017-03-14 03:24:18 +08:00
* @ private
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 18:35:04 +08:00
function changeOpenDiscussion ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 18:35:04 +08:00
if ( $openDiscussion . is ( ':checked' ) ) {
2017-02-13 01:08:08 +08:00
$burnAfterReadingOption . addClass ( 'buttondisabled' ) ;
2017-02-13 18:35:04 +08:00
$burnAfterReading . prop ( 'checked' , false ) ;
// if button is actually disabled, force-enable it and uncheck other button
$openDiscussionOption . removeClass ( 'buttondisabled' ) ;
} else {
2017-02-13 01:08:08 +08:00
$burnAfterReadingOption . removeClass ( 'buttondisabled' ) ;
2017-02-09 03:12:22 +08:00
}
2017-02-13 01:08:08 +08:00
}
2020-04-23 18:07:08 +08:00
/ * *
* Clear the attachment input in the top navigation .
*
* @ name TopNav . clearAttachmentInput
* @ function
* /
function clearAttachmentInput ( )
{
// hide UI for selected files
// our up-to-date jQuery can handle it :)
$fileWrap . find ( 'input' ) . val ( '' ) ;
}
2017-02-13 01:08:08 +08:00
/ * *
* return raw text
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . rawText
2017-03-14 03:24:18 +08:00
* @ private
2017-02-13 01:08:08 +08:00
* @ function
* /
2018-01-06 20:32:07 +08:00
function rawText ( )
2017-02-13 01:08:08 +08:00
{
2017-02-18 05:26:39 +08:00
TopNav . hideAllButtons ( ) ;
2018-01-06 20:32:07 +08:00
Alert . showLoading ( 'Showing raw text…' , 'time' ) ;
2018-12-30 01:40:59 +08:00
let paste = PasteViewer . getText ( ) ;
2017-02-16 05:59:55 +08:00
// push a new state to allow back navigation with browser back button
2017-02-13 01:08:08 +08:00
history . pushState (
2017-02-16 05:59:55 +08:00
{ type : 'raw' } ,
document . title ,
// recreate paste URL
Helper . baseUri ( ) + '?' + Model . getPasteId ( ) + '#' +
2019-05-16 03:20:54 +08:00
CryptTool . base58encode ( Model . getPasteKey ( ) )
2017-02-13 01:08:08 +08:00
) ;
2017-02-16 05:59:55 +08:00
2017-02-13 01:08:08 +08:00
// we use text/html instead of text/plain to avoid a bug when
// reloading the raw text view (it reverts to type text/html)
2018-12-30 01:40:59 +08:00
const $head = $ ( 'head' ) . children ( ) . not ( 'noscript, script, link[type="text/css"]' ) ,
newDoc = document . open ( 'text/html' , 'replace' ) ;
2017-02-18 05:26:39 +08:00
newDoc . write ( '<!DOCTYPE html><html><head>' ) ;
2018-12-30 01:40:59 +08:00
for ( let i = 0 ; i < $head . length ; ++ i ) {
2017-02-18 05:26:39 +08:00
newDoc . write ( $head [ i ] . outerHTML ) ;
}
2020-01-04 18:34:16 +08:00
newDoc . write ( '</head><body><pre>' + DOMPurify . sanitize ( Helper . htmlEntities ( paste ) ) + '</pre></body></html>' ) ;
2017-02-13 01:08:08 +08:00
newDoc . close ( ) ;
}
/ * *
2017-02-15 05:21:55 +08:00
* saves the language in a cookie and reloads the page
2017-02-13 01:08:08 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . setLanguage
2017-03-14 03:24:18 +08:00
* @ private
2017-02-13 01:08:08 +08:00
* @ function
* @ param { Event } event
* /
function setLanguage ( event )
{
2021-04-17 02:15:12 +08:00
document . cookie = 'lang=' + $ ( event . target ) . data ( 'lang' ) + ';secure' ;
2017-02-15 05:21:55 +08:00
UiHelper . reloadHome ( ) ;
2017-02-13 01:08:08 +08:00
}
2017-02-16 05:59:55 +08:00
/ * *
* hides all messages and creates a new paste
*
2017-03-14 03:24:18 +08:00
* @ name TopNav . clickNewPaste
2017-02-16 05:59:55 +08:00
* @ private
* @ function
* /
2018-01-06 20:32:07 +08:00
function clickNewPaste ( )
2017-02-16 05:59:55 +08:00
{
Controller . hideStatusMessages ( ) ;
Controller . newPaste ( ) ;
}
2017-04-12 04:21:30 +08:00
/ * *
* retrys some callback registered before
*
* @ name TopNav . clickRetryButton
* @ private
* @ function
* @ param { Event } event
* /
function clickRetryButton ( event )
{
2017-04-12 04:36:25 +08:00
retryButtonCallback ( event ) ;
2017-04-12 04:21:30 +08:00
}
2017-02-16 05:59:55 +08:00
/ * *
* removes the existing attachment
*
2017-03-14 03:24:18 +08:00
* @ name TopNav . removeAttachment
2017-02-16 05:59:55 +08:00
* @ private
* @ function
* @ param { Event } event
* /
function removeAttachment ( event )
{
// if custom attachment is used, remove it first
if ( ! $customAttachment . hasClass ( 'hidden' ) ) {
AttachmentViewer . removeAttachment ( ) ;
$customAttachment . addClass ( 'hidden' ) ;
$fileWrap . removeClass ( 'hidden' ) ;
}
2018-05-22 01:32:01 +08:00
// in any case, remove saved attachment data
AttachmentViewer . removeAttachmentData ( ) ;
2020-04-23 18:07:08 +08:00
clearAttachmentInput ( ) ;
2018-05-22 06:41:02 +08:00
AttachmentViewer . clearDragAndDrop ( ) ;
2017-02-16 05:59:55 +08:00
// pevent '#' from appearing in the URL
event . preventDefault ( ) ;
}
2017-02-09 03:12:22 +08:00
/ * *
2017-12-25 21:59:15 +08:00
* Shows the QR code of the current paste ( URL ) .
*
* @ name TopNav . displayQrCode
2018-03-03 14:55:27 +08:00
* @ private
2017-12-25 21:59:15 +08:00
* @ function
* /
2018-01-06 20:32:07 +08:00
function displayQrCode ( )
2017-12-25 21:59:15 +08:00
{
2018-12-30 01:40:59 +08:00
const qrCanvas = kjua ( {
2017-12-25 21:59:15 +08:00
render : 'canvas' ,
text : window . location . href
} ) ;
$ ( '#qrcode-display' ) . html ( qrCanvas ) ;
2018-01-06 16:58:19 +08:00
}
2017-12-25 21:59:15 +08:00
2019-08-22 05:36:22 +08:00
/ * *
* Template Email body .
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . templateEmailBody
2020-04-23 17:25:24 +08:00
* @ private
* @ param { string } expirationDateString
* @ param { bool } isBurnafterreading
2019-08-22 05:36:22 +08:00
* /
function templateEmailBody ( expirationDateString , isBurnafterreading )
{
const EOL = '\n' ;
const BULLET = ' - ' ;
let emailBody = '' ;
if ( expirationDateString !== null || isBurnafterreading ) {
emailBody += I18n . _ ( 'Notice:' ) ;
emailBody += EOL ;
if ( expirationDateString !== null ) {
emailBody += EOL ;
emailBody += BULLET ;
2020-05-30 17:52:15 +08:00
// avoid DOMPurify mess with forward slash in expirationDateString
emailBody += Helper . sprintf (
I18n . _ (
'This link will expire after %s.' ,
'%s'
) ,
2019-08-22 05:36:22 +08:00
expirationDateString
) ;
}
if ( isBurnafterreading ) {
emailBody += EOL ;
emailBody += BULLET ;
emailBody += I18n . _ (
'This link can only be accessed once, do not use back or refresh button in your browser.'
) ;
}
emailBody += EOL ;
emailBody += EOL ;
}
emailBody += I18n . _ ( 'Link:' ) ;
emailBody += EOL ;
emailBody += ` ${ window . location . href } ` ;
return emailBody ;
}
/ * *
* Trigger Email send .
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . triggerEmailSend
2020-04-23 17:25:24 +08:00
* @ private
* @ param { string } emailBody
2019-08-22 05:36:22 +08:00
* /
function triggerEmailSend ( emailBody )
{
window . open (
` mailto:?body= ${ encodeURIComponent ( emailBody ) } ` ,
'_self' ,
'noopener, noreferrer'
) ;
}
/ * *
* Send Email with current paste ( URL ) .
*
* @ name TopNav . sendEmail
* @ private
* @ function
* @ param { Date | null } expirationDate date of expiration
* @ param { bool } isBurnafterreading whether it is burn after reading
* /
function sendEmail ( expirationDate , isBurnafterreading )
{
const expirationDateRoundedToSecond = new Date ( expirationDate ) ;
// round down at least 30 seconds to make up for the delay of request
expirationDateRoundedToSecond . setUTCSeconds (
expirationDateRoundedToSecond . getUTCSeconds ( ) - 30
) ;
expirationDateRoundedToSecond . setUTCSeconds ( 0 ) ;
const $emailconfirmmodal = $ ( '#emailconfirmmodal' ) ;
if ( $emailconfirmmodal . length > 0 ) {
if ( expirationDate !== null ) {
2020-01-25 15:13:36 +08:00
I18n . _ (
$emailconfirmmodal . find ( '#emailconfirm-display' ) ,
'Recipient may become aware of your timezone, convert time to UTC?'
2019-08-22 05:36:22 +08:00
) ;
const $emailconfirmTimezoneCurrent = $emailconfirmmodal . find ( '#emailconfirm-timezone-current' ) ;
const $emailconfirmTimezoneUtc = $emailconfirmmodal . find ( '#emailconfirm-timezone-utc' ) ;
$emailconfirmTimezoneCurrent . off ( 'click.sendEmailCurrentTimezone' ) ;
2019-11-22 01:25:32 +08:00
$emailconfirmTimezoneCurrent . on ( 'click.sendEmailCurrentTimezone' , ( ) => {
const emailBody = templateEmailBody ( expirationDateRoundedToSecond . toLocaleString ( ) , isBurnafterreading ) ;
$emailconfirmmodal . modal ( 'hide' ) ;
triggerEmailSend ( emailBody ) ;
} ) ;
2019-08-22 05:36:22 +08:00
$emailconfirmTimezoneUtc . off ( 'click.sendEmailUtcTimezone' ) ;
2019-11-22 01:25:32 +08:00
$emailconfirmTimezoneUtc . on ( 'click.sendEmailUtcTimezone' , ( ) => {
const emailBody = templateEmailBody ( expirationDateRoundedToSecond . toLocaleString (
undefined ,
// we don't use Date.prototype.toUTCString() because we would like to avoid GMT
{ timeZone : 'UTC' , dateStyle : 'long' , timeStyle : 'long' }
) , isBurnafterreading ) ;
$emailconfirmmodal . modal ( 'hide' ) ;
triggerEmailSend ( emailBody ) ;
} ) ;
2019-08-22 05:36:22 +08:00
$emailconfirmmodal . modal ( 'show' ) ;
} else {
triggerEmailSend ( templateEmailBody ( null , isBurnafterreading ) ) ;
}
} else {
let emailBody = '' ;
if ( expirationDate !== null ) {
const expirationDateString = window . confirm (
I18n . _ ( 'Recipient may become aware of your timezone, convert time to UTC?' )
) ? expirationDateRoundedToSecond . toLocaleString (
undefined ,
// we don't use Date.prototype.toUTCString() because we would like to avoid GMT
{ timeZone : 'UTC' , dateStyle : 'long' , timeStyle : 'long' }
) : expirationDateRoundedToSecond . toLocaleString ( ) ;
emailBody = templateEmailBody ( expirationDateString , isBurnafterreading ) ;
} else {
emailBody = templateEmailBody ( null , isBurnafterreading ) ;
}
triggerEmailSend ( emailBody ) ;
}
}
2017-02-09 03:12:22 +08:00
/ * *
2018-02-25 16:45:51 +08:00
* Shows all navigation elements for viewing an existing paste
2017-02-09 03:12:22 +08:00
*
2017-02-18 05:26:39 +08:00
* @ name TopNav . showViewButtons
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-13 01:08:08 +08:00
me . showViewButtons = function ( )
2015-09-05 23:12:11 +08:00
{
2017-02-13 04:13:04 +08:00
if ( viewButtonsDisplayed ) {
return ;
}
2017-02-18 03:46:10 +08:00
$newButton . removeClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$cloneButton . removeClass ( 'hidden' ) ;
$rawTextButton . removeClass ( 'hidden' ) ;
2017-12-25 21:59:15 +08:00
$qrCodeLink . removeClass ( 'hidden' ) ;
2017-02-13 04:13:04 +08:00
viewButtonsDisplayed = true ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2017-02-13 01:08:08 +08:00
/ * *
2018-02-25 16:45:51 +08:00
* Hides all navigation elements for viewing an existing paste
2017-02-13 01:08:08 +08:00
*
2017-02-18 05:26:39 +08:00
* @ name TopNav . hideViewButtons
2017-02-13 01:08:08 +08:00
* @ function
* /
me . hideViewButtons = function ( )
{
2017-02-13 04:13:04 +08:00
if ( ! viewButtonsDisplayed ) {
return ;
}
2017-02-13 01:08:08 +08:00
$cloneButton . addClass ( 'hidden' ) ;
2017-04-12 04:21:30 +08:00
$newButton . addClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$rawTextButton . addClass ( 'hidden' ) ;
2017-12-25 21:59:15 +08:00
$qrCodeLink . addClass ( 'hidden' ) ;
2019-08-22 05:36:22 +08:00
me . hideEmailButton ( ) ;
2017-02-13 04:13:04 +08:00
viewButtonsDisplayed = false ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2017-02-18 05:26:39 +08:00
/ * *
* Hides all elements belonging to existing pastes
*
* @ name TopNav . hideAllButtons
* @ function
* /
me . hideAllButtons = function ( )
{
me . hideViewButtons ( ) ;
me . hideCreateButtons ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 05:26:39 +08:00
2017-02-13 01:08:08 +08:00
/ * *
* shows all elements needed when creating a new paste
*
2017-02-18 03:46:10 +08:00
* @ name TopNav . showCreateButtons
2017-02-13 01:08:08 +08:00
* @ function
* /
me . showCreateButtons = function ( )
{
2017-02-13 04:13:04 +08:00
if ( createButtonsDisplayed ) {
return ;
}
2017-04-12 04:21:30 +08:00
$attach . removeClass ( 'hidden' ) ;
$burnAfterReadingOption . removeClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$expiration . removeClass ( 'hidden' ) ;
$formatter . removeClass ( 'hidden' ) ;
$newButton . removeClass ( 'hidden' ) ;
2017-04-12 04:21:30 +08:00
$openDiscussionOption . removeClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$password . removeClass ( 'hidden' ) ;
2017-04-12 04:21:30 +08:00
$sendButton . removeClass ( 'hidden' ) ;
2017-02-13 04:13:04 +08:00
createButtonsDisplayed = true ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2017-02-13 01:08:08 +08:00
/ * *
* shows all elements needed when creating a new paste
*
2017-02-18 03:46:10 +08:00
* @ name TopNav . hideCreateButtons
2017-02-13 01:08:08 +08:00
* @ function
* /
me . hideCreateButtons = function ( )
{
2017-02-13 04:13:04 +08:00
if ( ! createButtonsDisplayed ) {
return ;
}
2017-02-15 05:21:55 +08:00
$newButton . addClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$sendButton . addClass ( 'hidden' ) ;
$expiration . addClass ( 'hidden' ) ;
$formatter . addClass ( 'hidden' ) ;
$burnAfterReadingOption . addClass ( 'hidden' ) ;
2017-02-13 18:35:04 +08:00
$openDiscussionOption . addClass ( 'hidden' ) ;
2017-02-13 01:08:08 +08:00
$password . addClass ( 'hidden' ) ;
$attach . addClass ( 'hidden' ) ;
2017-02-13 04:13:04 +08:00
2017-02-15 05:21:55 +08:00
createButtonsDisplayed = false ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
/ * *
* only shows the "new paste" button
*
2017-02-18 03:46:10 +08:00
* @ name TopNav . showNewPasteButton
2017-02-15 05:21:55 +08:00
* @ function
* /
me . showNewPasteButton = function ( )
{
$newButton . removeClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-04-12 04:21:30 +08:00
/ * *
* only shows the "retry" button
*
* @ name TopNav . showRetryButton
* @ function
* /
me . showRetryButton = function ( )
{
$retryButton . removeClass ( 'hidden' ) ;
}
/ * *
* hides the "retry" button
*
* @ name TopNav . hideRetryButton
* @ function
* /
me . hideRetryButton = function ( )
{
$retryButton . addClass ( 'hidden' ) ;
}
2019-08-22 05:36:22 +08:00
/ * *
* show the "email" button
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . showEmailbutton
* @ function
* @ param { int | undefined } optionalRemainingTimeInSeconds
* /
me . showEmailButton = function ( optionalRemainingTimeInSeconds )
{
try {
// we cache expiration date in closure to avoid inaccurate expiration datetime
const expirationDate = Helper . calculateExpirationDate (
new Date ( ) ,
typeof optionalRemainingTimeInSeconds === 'number' ? optionalRemainingTimeInSeconds : TopNav . getExpiration ( )
) ;
const isBurnafterreading = TopNav . getBurnAfterReading ( ) ;
$emailLink . removeClass ( 'hidden' ) ;
$emailLink . off ( 'click.sendEmail' ) ;
2019-11-22 01:25:32 +08:00
$emailLink . on ( 'click.sendEmail' , ( ) => {
sendEmail ( expirationDate , isBurnafterreading ) ;
} ) ;
2019-08-22 05:36:22 +08:00
} catch ( error ) {
console . error ( error ) ;
2020-01-25 15:13:36 +08:00
Alert . showError ( 'Cannot calculate expiration date.' ) ;
2019-08-22 05:36:22 +08:00
}
}
/ * *
* hide the "email" button
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . hideEmailButton
* @ function
* /
me . hideEmailButton = function ( )
{
$emailLink . addClass ( 'hidden' ) ;
$emailLink . off ( 'click.sendEmail' ) ;
}
2017-02-15 05:21:55 +08:00
/ * *
* only hides the clone button
*
* @ name TopNav . hideCloneButton
* @ function
* /
me . hideCloneButton = function ( )
{
$cloneButton . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2012-04-23 22:30:02 +08:00
2017-02-13 01:08:08 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* only hides the raw text button
2017-02-13 01:08:08 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . hideRawButton
2017-02-13 01:08:08 +08:00
* @ function
* /
2017-02-15 05:21:55 +08:00
me . hideRawButton = function ( )
2017-02-13 01:08:08 +08:00
{
2017-02-15 05:21:55 +08:00
$rawTextButton . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2012-04-23 22:30:02 +08:00
2019-08-22 05:36:22 +08:00
/ * *
* only hides the qr code button
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . hideQrCodeButton
* @ function
* /
me . hideQrCodeButton = function ( )
{
$qrCodeLink . addClass ( 'hidden' ) ;
}
/ * *
* hide all irrelevant buttons when viewing burn after reading paste
2020-04-23 17:25:24 +08:00
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . hideBurnAfterReadingButtons
* @ function
* /
me . hideBurnAfterReadingButtons = function ( )
{
me . hideCloneButton ( ) ;
me . hideQrCodeButton ( ) ;
me . hideEmailButton ( ) ;
}
2017-02-09 03:12:22 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* hides the file selector in attachment
2017-02-09 03:12:22 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name TopNav . hideFileSelector
2017-02-09 03:12:22 +08:00
* @ function
* /
2017-02-16 05:59:55 +08:00
me . hideFileSelector = function ( )
2017-02-09 03:11:04 +08:00
{
2017-02-16 05:59:55 +08:00
$fileWrap . addClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 01:08:08 +08:00
2017-02-16 05:59:55 +08:00
2017-02-13 01:08:08 +08:00
/ * *
2017-02-16 05:59:55 +08:00
* shows the custom attachment
2017-02-13 01:08:08 +08:00
*
2017-02-16 05:59:55 +08:00
* @ name TopNav . showCustomAttachment
2017-02-13 01:08:08 +08:00
* @ function
* /
2017-02-16 05:59:55 +08:00
me . showCustomAttachment = function ( )
2017-02-13 01:08:08 +08:00
{
2017-02-16 05:59:55 +08:00
$customAttachment . removeClass ( 'hidden' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
2019-08-18 10:17:35 +08:00
/ * *
* hides the custom attachment
2020-04-23 17:25:24 +08:00
*
2019-08-18 10:17:35 +08:00
* @ name TopNav . hideCustomAttachment
* @ function
* /
me . hideCustomAttachment = function ( )
{
$customAttachment . addClass ( 'hidden' ) ;
$fileWrap . removeClass ( 'hidden' ) ;
} ;
2017-02-14 04:12:00 +08:00
/ * *
2018-03-04 18:47:58 +08:00
* collapses the navigation bar , only if expanded
2017-02-14 04:12:00 +08:00
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . collapseBar
2017-02-14 04:12:00 +08:00
* @ function
* /
me . collapseBar = function ( )
{
2018-03-04 20:19:49 +08:00
if ( $ ( '#navbar' ) . attr ( 'aria-expanded' ) === 'true' ) {
2018-03-03 14:55:27 +08:00
$ ( '.navbar-toggle' ) . click ( ) ;
2017-02-14 04:12:00 +08:00
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-09 03:12:22 +08:00
2020-04-23 17:25:24 +08:00
/ * *
2020-04-23 18:07:08 +08:00
* Reset the top navigation back to it ' s default values .
2020-04-23 17:25:24 +08:00
*
2020-04-23 18:07:08 +08:00
* @ name TopNav . resetInput
2020-04-23 17:25:24 +08:00
* @ function
* /
2020-04-23 18:07:08 +08:00
me . resetInput = function ( )
2020-04-23 17:25:24 +08:00
{
2020-04-23 18:07:08 +08:00
clearAttachmentInput ( ) ;
2021-04-05 19:24:53 +08:00
$burnAfterReading . prop ( 'checked' , burnAfterReadingDefault ) ;
$openDiscussion . prop ( 'checked' , openDiscussionDefault ) ;
2021-04-05 19:47:37 +08:00
if ( openDiscussionDefault || ! burnAfterReadingDefault ) $openDiscussionOption . removeClass ( 'buttondisabled' ) ;
if ( burnAfterReadingDefault || ! openDiscussionDefault ) $burnAfterReadingOption . removeClass ( 'buttondisabled' ) ;
pasteExpiration = Model . getExpirationDefault ( ) || pasteExpiration ;
$ ( '#pasteExpiration>option' ) . each ( function ( ) {
const $this = $ ( this ) ;
if ( $this . val ( ) === pasteExpiration ) {
$ ( '#pasteExpirationDisplay' ) . text ( $this . text ( ) ) ;
}
} ) ;
2020-04-23 17:25:24 +08:00
} ;
2017-02-13 18:35:04 +08:00
/ * *
* returns the currently set expiration time
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . getExpiration
2017-02-13 18:35:04 +08:00
* @ function
* @ return { int }
* /
me . getExpiration = function ( )
{
return pasteExpiration ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 18:35:04 +08:00
2017-02-14 04:12:00 +08:00
/ * *
* returns the currently selected file ( s )
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . getFileList
2017-02-14 04:12:00 +08:00
* @ function
* @ return { FileList | null }
* /
me . getFileList = function ( )
{
2018-12-30 01:40:59 +08:00
const $file = $ ( '#file' ) ;
2017-02-14 04:12:00 +08:00
// if no file given, return null
if ( ! $file . length || ! $file [ 0 ] . files . length ) {
return null ;
}
2018-01-06 20:32:07 +08:00
// ensure the selected file is still accessible
2017-02-14 04:12:00 +08:00
if ( ! ( $file [ 0 ] . files && $file [ 0 ] . files [ 0 ] ) ) {
return null ;
}
return $file [ 0 ] . files ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* returns the state of the burn after reading checkbox
*
2019-08-22 05:36:22 +08:00
* @ name TopNav . getBurnAfterReading
2017-02-14 04:12:00 +08:00
* @ function
* @ return { bool }
* /
me . getBurnAfterReading = function ( )
{
return $burnAfterReading . is ( ':checked' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* returns the state of the discussion checkbox
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . getOpenDiscussion
2017-02-14 04:12:00 +08:00
* @ function
* @ return { bool }
* /
me . getOpenDiscussion = function ( )
{
return $openDiscussion . is ( ':checked' ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* returns the entered password
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . getPassword
2017-02-14 04:12:00 +08:00
* @ function
* @ return { string }
* /
me . getPassword = function ( )
{
2019-10-26 01:05:09 +08:00
// when password is disabled $passwordInput.val() will return undefined
return $passwordInput . val ( ) || '' ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* returns the element where custom attachments can be placed
*
* Used by AttachmentViewer when an attachment is cloned here .
*
* @ name TopNav . getCustomAttachment
* @ function
* @ return { jQuery }
* /
me . getCustomAttachment = function ( )
{
return $customAttachment ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-16 05:59:55 +08:00
2017-04-12 04:21:30 +08:00
/ * *
* Set a function to call when the retry button is clicked .
*
* @ name TopNav . setRetryCallback
* @ function
* @ param { function } callback
* /
me . setRetryCallback = function ( callback )
{
retryButtonCallback = callback ;
}
2019-08-16 07:28:42 +08:00
/ * *
* Highlight file upload
2020-04-23 17:25:24 +08:00
*
2019-08-16 07:28:42 +08:00
* @ name TopNav . highlightFileupload
* @ function
* /
me . highlightFileupload = function ( )
{
// visually indicate file uploaded
const $attachDropdownToggle = $attach . children ( '.dropdown-toggle' ) ;
if ( $attachDropdownToggle . attr ( 'aria-expanded' ) === 'false' ) {
$attachDropdownToggle . click ( ) ;
}
$fileWrap . addClass ( 'highlight' ) ;
setTimeout ( function ( ) {
$fileWrap . removeClass ( 'highlight' ) ;
} , 300 ) ;
}
2019-08-18 10:17:35 +08:00
/ * *
* set the format on bootstrap templates in dropdown programmatically
2020-04-23 17:25:24 +08:00
*
2019-08-18 10:17:35 +08:00
* @ name TopNav . setFormat
* @ function
* /
me . setFormat = function ( format )
{
$formatter . parent ( ) . find ( ` a[data-format=" ${ format } "] ` ) . click ( ) ;
}
2019-08-26 00:00:59 +08:00
/ * *
* returns if attachment dropdown is readonly , not editable
2020-04-23 17:25:24 +08:00
*
2019-08-26 00:00:59 +08:00
* @ name TopNav . isAttachmentReadonly
* @ function
* @ return { bool }
* /
me . isAttachmentReadonly = function ( )
{
2020-05-30 17:48:15 +08:00
return ! createButtonsDisplayed || $attach . hasClass ( 'hidden' ) ;
2019-08-26 00:00:59 +08:00
}
2017-02-13 01:08:08 +08:00
/ * *
* init navigation manager
*
* preloads jQuery elements
*
2017-02-15 05:21:55 +08:00
* @ name TopNav . init
2017-02-13 01:08:08 +08:00
* @ function
* /
me . init = function ( )
{
2017-02-09 03:12:22 +08:00
$attach = $ ( '#attach' ) ;
$burnAfterReading = $ ( '#burnafterreading' ) ;
$burnAfterReadingOption = $ ( '#burnafterreadingoption' ) ;
$cloneButton = $ ( '#clonebutton' ) ;
2017-02-16 05:59:55 +08:00
$customAttachment = $ ( '#customattachment' ) ;
2017-02-09 03:12:22 +08:00
$expiration = $ ( '#expiration' ) ;
$fileRemoveButton = $ ( '#fileremovebutton' ) ;
2017-02-18 05:46:18 +08:00
$fileWrap = $ ( '#filewrap' ) ;
2017-02-09 03:12:22 +08:00
$formatter = $ ( '#formatter' ) ;
$newButton = $ ( '#newbutton' ) ;
$openDiscussion = $ ( '#opendiscussion' ) ;
2017-02-18 05:46:18 +08:00
$openDiscussionOption = $ ( '#opendiscussionoption' ) ;
2017-02-13 04:13:04 +08:00
$password = $ ( '#password' ) ;
2017-02-14 04:12:00 +08:00
$passwordInput = $ ( '#passwordinput' ) ;
2017-02-09 03:12:22 +08:00
$rawTextButton = $ ( '#rawtextbutton' ) ;
2017-04-12 04:21:30 +08:00
$retryButton = $ ( '#retrybutton' ) ;
2017-02-09 03:12:22 +08:00
$sendButton = $ ( '#sendbutton' ) ;
2017-12-25 21:59:15 +08:00
$qrCodeLink = $ ( '#qrcodelink' ) ;
2019-08-22 05:36:22 +08:00
$emailLink = $ ( '#emaillink' ) ;
2013-11-01 08:15:14 +08:00
2017-02-13 01:08:08 +08:00
// bootstrap template drop down
2017-02-18 03:46:10 +08:00
$ ( '#language ul.dropdown-menu li a' ) . click ( setLanguage ) ;
2017-02-13 01:08:08 +08:00
// page template drop down
2017-02-18 03:46:10 +08:00
$ ( '#language select option' ) . click ( setLanguage ) ;
2017-02-13 01:08:08 +08:00
// bind events
$burnAfterReading . change ( changeBurnAfterReading ) ;
2017-02-13 18:35:04 +08:00
$openDiscussionOption . change ( changeOpenDiscussion ) ;
2017-02-16 05:59:55 +08:00
$newButton . click ( clickNewPaste ) ;
2017-03-14 03:24:18 +08:00
$sendButton . click ( PasteEncrypter . sendPaste ) ;
2017-02-15 05:21:55 +08:00
$cloneButton . click ( Controller . clonePaste ) ;
2017-02-13 04:13:04 +08:00
$rawTextButton . click ( rawText ) ;
2017-04-12 04:21:30 +08:00
$retryButton . click ( clickRetryButton ) ;
2017-02-16 05:59:55 +08:00
$fileRemoveButton . click ( removeAttachment ) ;
2017-12-25 21:59:15 +08:00
$qrCodeLink . click ( displayQrCode ) ;
2017-02-13 01:08:08 +08:00
2017-02-13 18:35:04 +08:00
// bootstrap template drop downs
$ ( 'ul.dropdown-menu li a' , $ ( '#expiration' ) . parent ( ) ) . click ( updateExpiration ) ;
$ ( 'ul.dropdown-menu li a' , $ ( '#formatter' ) . parent ( ) ) . click ( updateFormat ) ;
2017-02-13 01:08:08 +08:00
// initiate default state of checkboxes
changeBurnAfterReading ( ) ;
2017-02-13 18:35:04 +08:00
changeOpenDiscussion ( ) ;
2021-04-05 19:24:53 +08:00
// get default values from template or fall back to set value
burnAfterReadingDefault = me . getBurnAfterReading ( ) ;
openDiscussionDefault = me . getOpenDiscussion ( ) ;
2017-02-16 05:59:55 +08:00
pasteExpiration = Model . getExpirationDefault ( ) || pasteExpiration ;
2018-03-01 13:43:30 +08:00
createButtonsDisplayed = false ;
viewButtonsDisplayed = false ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 01:08:08 +08:00
return me ;
} ) ( window , document ) ;
/ * *
2017-02-14 04:12:00 +08:00
* Responsible for AJAX requests , transparently handles encryption …
2017-02-13 01:08:08 +08:00
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction
2017-02-13 01:08:08 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const ServerInteraction = ( function ( ) {
const me = { } ;
2017-02-13 01:08:08 +08:00
2018-12-30 01:40:59 +08:00
let successFunc = null ,
2017-02-15 05:21:55 +08:00
failureFunc = null ,
2018-12-26 00:34:39 +08:00
symmetricKey = null ,
2017-02-15 05:21:55 +08:00
url ,
data ,
2017-02-14 04:12:00 +08:00
password ;
2017-02-15 05:21:55 +08:00
/ * *
* public variable ( 'constant' ) for errors to prevent magic numbers
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . error
2017-02-15 05:21:55 +08:00
* @ readonly
* @ enum { Object }
* /
2017-02-14 04:12:00 +08:00
me . error = {
okay : 0 ,
custom : 1 ,
unknown : 2 ,
serverError : 3
} ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* ajaxHeaders to send in AJAX requests
2017-02-13 01:08:08 +08:00
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . ajaxHeaders
2017-02-13 01:08:08 +08:00
* @ private
2017-02-14 04:12:00 +08:00
* @ readonly
2017-02-13 01:08:08 +08:00
* @ enum { Object }
* /
2018-12-30 01:40:59 +08:00
const ajaxHeaders = { 'X-Requested-With' : 'JSONHttpRequest' } ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* called after successful upload
2017-02-13 01:08:08 +08:00
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . success
2017-02-18 03:46:10 +08:00
* @ private
2017-02-14 04:12:00 +08:00
* @ function
* @ param { int } status
2017-03-25 07:58:59 +08:00
* @ param { int } result - optional
2017-02-13 01:08:08 +08:00
* /
2017-02-14 04:12:00 +08:00
function success ( status , result )
{
if ( successFunc !== null ) {
2019-05-25 19:20:39 +08:00
// add useful data to result
result . encryptionKey = symmetricKey ;
2017-02-14 04:12:00 +08:00
successFunc ( status , result ) ;
}
}
2017-02-13 01:08:08 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* called after a upload failure
2017-02-13 01:08:08 +08:00
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . fail
2017-02-18 03:46:10 +08:00
* @ private
2017-02-14 04:12:00 +08:00
* @ function
* @ param { int } status - internal code
2017-03-25 07:58:59 +08:00
* @ param { int } result - original error code
2017-02-13 01:08:08 +08:00
* /
2017-02-14 04:12:00 +08:00
function fail ( status , result )
{
if ( failureFunc !== null ) {
failureFunc ( status , result ) ;
}
}
/ * *
* actually uploads the data
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . run
2017-02-14 04:12:00 +08:00
* @ function
* /
2017-02-15 05:21:55 +08:00
me . run = function ( )
2017-02-14 04:12:00 +08:00
{
2019-05-11 16:39:42 +08:00
let isPost = Object . keys ( data ) . length > 0 ,
ajaxParams = {
2019-05-14 04:31:52 +08:00
type : isPost ? 'POST' : 'GET' ,
url : url ,
headers : ajaxHeaders ,
dataType : 'json' ,
success : function ( result ) {
if ( result . status === 0 ) {
success ( 0 , result ) ;
} else if ( result . status === 1 ) {
fail ( 1 , result ) ;
} else {
fail ( 2 , result ) ;
}
2017-02-14 04:12:00 +08:00
}
2019-05-14 04:31:52 +08:00
} ;
2019-05-11 16:39:42 +08:00
if ( isPost ) {
2019-05-14 04:31:52 +08:00
ajaxParams . data = JSON . stringify ( data ) ;
2019-05-11 16:39:42 +08:00
}
$ . ajax ( ajaxParams ) . fail ( function ( jqXHR , textStatus , errorThrown ) {
2017-02-14 04:12:00 +08:00
console . error ( textStatus , errorThrown ) ;
fail ( 3 , jqXHR ) ;
} ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
2019-04-16 13:45:04 +08:00
/ * *
* return currently set data , used in unit testing
*
* @ name ServerInteraction . getData
* @ function
* /
me . getData = function ( )
{
return data ;
} ;
2017-02-14 04:12:00 +08:00
/ * *
* set success function
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setUrl
2017-02-15 05:21:55 +08:00
* @ function
2017-03-25 07:58:59 +08:00
* @ param { function } newUrl
2017-02-15 05:21:55 +08:00
* /
me . setUrl = function ( newUrl )
{
url = newUrl ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
2017-02-18 03:46:10 +08:00
/ * *
* sets the password to use ( first value ) and optionally also the
2018-12-26 00:34:39 +08:00
* encryption key ( not recommended , it is automatically generated ) .
2017-02-18 03:46:10 +08:00
*
* Note : Call this after prepare ( ) as prepare ( ) resets these values .
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setCryptValues
2017-02-18 03:46:10 +08:00
* @ function
* @ param { string } newPassword
* @ param { string } newKey - optional
* /
me . setCryptParameters = function ( newPassword , newKey )
{
password = newPassword ;
if ( typeof newKey !== 'undefined' ) {
symmetricKey = newKey ;
}
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
2017-02-15 05:21:55 +08:00
/ * *
* set success function
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setSuccess
2017-02-14 04:12:00 +08:00
* @ function
* @ param { function } func
* /
me . setSuccess = function ( func )
{
successFunc = func ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* set failure function
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setFailure
2017-02-14 04:12:00 +08:00
* @ function
* @ param { function } func
* /
me . setFailure = function ( func )
{
failureFunc = func ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* prepares a new upload
*
2017-02-18 03:46:10 +08:00
* Call this when doing a new upload to reset any data from potential
* previous uploads . Must be called before any other method of this
* module .
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . prepare
2017-02-14 04:12:00 +08:00
* @ function
* @ return { object }
* /
2017-02-18 03:46:10 +08:00
me . prepare = function ( )
2017-02-14 04:12:00 +08:00
{
2017-02-15 05:21:55 +08:00
// entropy should already be checked!
2017-02-14 04:12:00 +08:00
2017-02-18 03:46:10 +08:00
// reset password
password = '' ;
// reset key, so it a new one is generated when it is used
symmetricKey = null ;
2017-02-14 04:12:00 +08:00
// reset data
2017-02-15 05:21:55 +08:00
successFunc = null ;
failureFunc = null ;
2017-02-18 03:46:10 +08:00
url = Helper . baseUri ( ) ;
2017-02-14 04:12:00 +08:00
data = { } ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* encrypts and sets the data
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setCipherMessage
2018-10-09 03:03:10 +08:00
* @ async
2017-02-14 04:12:00 +08:00
* @ function
2018-12-26 00:34:39 +08:00
* @ param { object } cipherMessage
2017-02-14 04:12:00 +08:00
* /
2018-12-26 00:34:39 +08:00
me . setCipherMessage = async function ( cipherMessage )
2017-02-14 04:12:00 +08:00
{
2018-12-26 00:34:39 +08:00
if (
symmetricKey === null ||
( typeof symmetricKey === 'string' && symmetricKey === '' )
) {
symmetricKey = CryptTool . getSymmetricKey ( ) ;
}
if ( ! data . hasOwnProperty ( 'adata' ) ) {
data [ 'adata' ] = [ ] ;
}
let cipherResult = await CryptTool . cipher ( symmetricKey , password , JSON . stringify ( cipherMessage ) , data [ 'adata' ] ) ;
data [ 'v' ] = 2 ;
data [ 'ct' ] = cipherResult [ 0 ] ;
data [ 'adata' ] = cipherResult [ 1 ] ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
* set the additional metadata to send unencrypted
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . setUnencryptedData
2017-02-14 04:12:00 +08:00
* @ function
* @ param { string } index
* @ param { mixed } element
* /
me . setUnencryptedData = function ( index , element )
{
data [ index ] = element ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
/ * *
2018-12-26 00:34:39 +08:00
* Helper , which parses shows a general error message based on the result of the ServerInteraction
2017-02-14 04:12:00 +08:00
*
2018-12-26 00:34:39 +08:00
* @ name ServerInteraction . parseUploadError
2017-02-18 03:46:10 +08:00
* @ function
* @ param { int } status
* @ param { object } data
* @ param { string } doThisThing - a human description of the action , which was tried
* @ return { array }
* /
me . parseUploadError = function ( status , data , doThisThing ) {
2018-12-30 01:40:59 +08:00
let errorArray ;
2017-02-18 03:46:10 +08:00
switch ( status ) {
2018-01-06 16:26:10 +08:00
case me . error . custom :
2017-02-18 03:46:10 +08:00
errorArray = [ 'Could not ' + doThisThing + ': %s' , data . message ] ;
break ;
2018-01-06 16:26:10 +08:00
case me . error . unknown :
2017-02-18 03:46:10 +08:00
errorArray = [ 'Could not ' + doThisThing + ': %s' , I18n . _ ( 'unknown status' ) ] ;
break ;
2018-01-06 16:26:10 +08:00
case me . error . serverError :
2017-03-25 07:58:59 +08:00
errorArray = [ 'Could not ' + doThisThing + ': %s' , I18n . _ ( 'server error or not responding' ) ] ;
break ;
2017-02-18 03:46:10 +08:00
default :
errorArray = [ 'Could not ' + doThisThing + ': %s' , I18n . _ ( 'unknown error' ) ] ;
break ;
}
return errorArray ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
2017-02-14 04:12:00 +08:00
return me ;
} ) ( ) ;
/ * *
2017-02-15 05:21:55 +08:00
* ( controller ) Responsible for encrypting paste and sending it to server .
2017-02-14 04:12:00 +08:00
*
2018-12-26 00:34:39 +08:00
* Does upload , encryption is done transparently by ServerInteraction .
2017-02-18 03:46:10 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteEncrypter
2017-02-14 04:12:00 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const PasteEncrypter = ( function ( ) {
const me = { } ;
2017-02-14 04:12:00 +08:00
/ * *
2017-02-18 03:46:10 +08:00
* called after successful paste upload
2017-02-14 04:12:00 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteEncrypter . showCreatedPaste
2017-02-15 05:21:55 +08:00
* @ private
2017-02-14 04:12:00 +08:00
* @ function
* @ param { int } status
2017-02-18 03:46:10 +08:00
* @ param { object } data
2017-02-14 04:12:00 +08:00
* /
function showCreatedPaste ( status , data ) {
2017-02-16 05:59:55 +08:00
Alert . hideLoading ( ) ;
2017-02-15 05:21:55 +08:00
Alert . hideMessages ( ) ;
2017-02-14 04:12:00 +08:00
// show notification
2018-12-30 01:40:59 +08:00
const baseUri = Helper . baseUri ( ) + '?' ,
2019-05-16 03:20:54 +08:00
url = baseUri + data . id + '#' + CryptTool . base58encode ( data . encryptionKey ) ,
2018-12-30 01:40:59 +08:00
deleteUrl = baseUri + 'pasteid=' + data . id + '&deletetoken=' + data . deletetoken ;
2018-01-06 16:26:10 +08:00
PasteStatus . createPasteNotification ( url , deleteUrl ) ;
2017-02-14 04:12:00 +08:00
// show new URL in browser bar
history . pushState ( { type : 'newpaste' } , document . title , url ) ;
2017-02-15 05:21:55 +08:00
TopNav . showViewButtons ( ) ;
2019-08-22 05:36:22 +08:00
// this cannot be grouped with showViewButtons due to remaining time calculation
TopNav . showEmailButton ( ) ;
2017-02-15 05:21:55 +08:00
TopNav . hideRawButton ( ) ;
Editor . hide ( ) ;
2017-02-14 04:12:00 +08:00
// parse and show text
2017-03-14 03:24:18 +08:00
// (preparation already done in me.sendPaste())
2017-02-15 05:21:55 +08:00
PasteViewer . run ( ) ;
}
2017-02-18 03:46:10 +08:00
/ * *
* called after successful comment upload
*
2017-03-14 03:24:18 +08:00
* @ name PasteEncrypter . showUploadedComment
2017-02-18 03:46:10 +08:00
* @ private
* @ function
* @ param { int } status
* @ param { object } data
* /
function showUploadedComment ( status , data ) {
// show success message
2018-01-06 20:32:07 +08:00
Alert . showStatus ( 'Comment posted.' ) ;
2017-02-18 03:46:10 +08:00
// reload paste
Controller . refreshPaste ( function ( ) {
// highlight sent comment
DiscussionViewer . highlightComment ( data . id , true ) ;
// reset error handler
Alert . setCustomHandler ( null ) ;
} ) ;
}
2017-02-13 01:08:08 +08:00
/ * *
* send a reply in a discussion
*
2017-02-15 05:21:55 +08:00
* @ name PasteEncrypter . sendComment
2018-10-20 23:57:21 +08:00
* @ async
2017-02-13 01:08:08 +08:00
* @ function
* /
2018-10-09 03:03:10 +08:00
me . sendComment = async function ( )
2017-02-13 01:08:08 +08:00
{
2017-02-18 03:46:10 +08:00
Alert . hideMessages ( ) ;
Alert . setCustomHandler ( DiscussionViewer . handleNotification ) ;
// UI loading state
2017-02-18 05:26:39 +08:00
TopNav . hideAllButtons ( ) ;
2018-01-06 20:32:07 +08:00
Alert . showLoading ( 'Sending comment…' , 'cloud-upload' ) ;
2017-02-18 03:46:10 +08:00
2017-12-18 21:47:17 +08:00
// get data
2018-12-30 01:40:59 +08:00
const plainText = DiscussionViewer . getReplyMessage ( ) ,
nickname = DiscussionViewer . getReplyNickname ( ) ,
parentid = DiscussionViewer . getReplyCommentId ( ) ;
2017-02-18 03:46:10 +08:00
// do not send if there is no data
if ( plainText . length === 0 ) {
// revert loading status…
Alert . hideLoading ( ) ;
Alert . setCustomHandler ( null ) ;
TopNav . showViewButtons ( ) ;
2017-02-13 01:08:08 +08:00
return ;
}
2018-12-26 00:34:39 +08:00
// prepare server interaction
ServerInteraction . prepare ( ) ;
ServerInteraction . setCryptParameters ( Prompt . getPassword ( ) , Model . getPasteKey ( ) ) ;
2017-02-18 03:46:10 +08:00
// set success/fail functions
2018-12-26 00:34:39 +08:00
ServerInteraction . setSuccess ( showUploadedComment ) ;
ServerInteraction . setFailure ( function ( status , data ) {
2017-02-18 03:46:10 +08:00
// revert loading status…
Alert . hideLoading ( ) ;
TopNav . showViewButtons ( ) ;
2018-10-20 17:40:37 +08:00
// …show error message…
2018-01-06 20:32:07 +08:00
Alert . showError (
2018-12-26 00:34:39 +08:00
ServerInteraction . parseUploadError ( status , data , 'post comment' )
2018-01-06 20:32:07 +08:00
) ;
2017-02-18 04:48:21 +08:00
2018-10-20 17:40:37 +08:00
// …and reset error handler
2017-02-18 04:48:21 +08:00
Alert . setCustomHandler ( null ) ;
2017-02-13 01:08:08 +08:00
} ) ;
2017-02-18 03:46:10 +08:00
// fill it with unencrypted params
2018-12-26 00:34:39 +08:00
ServerInteraction . setUnencryptedData ( 'pasteid' , Model . getPasteId ( ) ) ;
2017-02-18 03:46:10 +08:00
if ( typeof parentid === 'undefined' ) {
// if parent id is not set, this is the top-most comment, so use
2018-01-06 20:32:07 +08:00
// paste id as parent, as the root element of the discussion tree
2018-12-26 00:34:39 +08:00
ServerInteraction . setUnencryptedData ( 'parentid' , Model . getPasteId ( ) ) ;
2017-02-18 03:46:10 +08:00
} else {
2018-12-26 00:34:39 +08:00
ServerInteraction . setUnencryptedData ( 'parentid' , parentid ) ;
2017-02-18 03:46:10 +08:00
}
2018-12-26 00:34:39 +08:00
// prepare cypher message
let cipherMessage = {
'comment' : plainText
} ;
2018-10-20 17:40:37 +08:00
if ( nickname . length > 0 ) {
2018-12-26 00:34:39 +08:00
cipherMessage [ 'nickname' ] = nickname ;
2018-08-05 04:30:01 +08:00
}
2017-02-18 03:46:10 +08:00
2018-12-26 00:34:39 +08:00
await ServerInteraction . setCipherMessage ( cipherMessage ) . catch ( Alert . showError ) ;
2019-05-15 13:44:03 +08:00
ServerInteraction . run ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 01:08:08 +08:00
/ * *
2017-02-14 04:12:00 +08:00
* sends a new paste to server
2017-02-13 01:08:08 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteEncrypter . sendPaste
2018-10-20 23:57:21 +08:00
* @ async
2017-02-13 01:08:08 +08:00
* @ function
* /
2018-10-09 03:03:10 +08:00
me . sendPaste = async function ( )
2017-02-13 01:08:08 +08:00
{
2017-02-16 05:59:55 +08:00
// hide previous (error) messages
2017-02-18 03:46:10 +08:00
Controller . hideStatusMessages ( ) ;
2017-02-16 05:59:55 +08:00
2017-02-14 04:12:00 +08:00
// UI loading state
2017-02-18 05:26:39 +08:00
TopNav . hideAllButtons ( ) ;
2018-01-06 20:32:07 +08:00
Alert . showLoading ( 'Sending paste…' , 'cloud-upload' ) ;
2017-02-15 05:21:55 +08:00
TopNav . collapseBar ( ) ;
2017-02-13 01:08:08 +08:00
2017-02-14 04:12:00 +08:00
// get data
2018-12-30 01:40:59 +08:00
const plainText = Editor . getText ( ) ,
format = PasteViewer . getFormat ( ) ,
// the methods may return different values if no files are attached (null, undefined or false)
files = TopNav . getFileList ( ) || AttachmentViewer . getFile ( ) || AttachmentViewer . hasAttachment ( ) ;
2017-02-14 04:12:00 +08:00
// do not send if there is no data
2018-05-22 01:32:01 +08:00
if ( plainText . length === 0 && ! files ) {
2017-02-14 04:12:00 +08:00
// revert loading status…
2017-02-16 05:59:55 +08:00
Alert . hideLoading ( ) ;
2017-02-15 05:21:55 +08:00
TopNav . showCreateButtons ( ) ;
2017-02-13 01:08:08 +08:00
return ;
}
2018-12-26 00:34:39 +08:00
// prepare server interaction
ServerInteraction . prepare ( ) ;
ServerInteraction . setCryptParameters ( TopNav . getPassword ( ) ) ;
2017-02-14 04:12:00 +08:00
// set success/fail functions
2018-12-26 00:34:39 +08:00
ServerInteraction . setSuccess ( showCreatedPaste ) ;
ServerInteraction . setFailure ( function ( status , data ) {
2017-02-13 01:08:08 +08:00
// revert loading status…
2017-02-16 05:59:55 +08:00
Alert . hideLoading ( ) ;
2017-02-15 05:21:55 +08:00
TopNav . showCreateButtons ( ) ;
2017-02-14 04:12:00 +08:00
// show error message
2018-01-06 20:32:07 +08:00
Alert . showError (
2018-12-26 00:34:39 +08:00
ServerInteraction . parseUploadError ( status , data , 'create paste' )
2018-01-06 20:32:07 +08:00
) ;
2017-02-13 01:08:08 +08:00
} ) ;
2017-02-14 04:12:00 +08:00
// fill it with unencrypted submitted options
2018-12-26 00:34:39 +08:00
ServerInteraction . setUnencryptedData ( 'adata' , [
null , format ,
TopNav . getOpenDiscussion ( ) ? 1 : 0 ,
TopNav . getBurnAfterReading ( ) ? 1 : 0
] ) ;
ServerInteraction . setUnencryptedData ( 'meta' , { 'expire' : TopNav . getExpiration ( ) } ) ;
2017-02-13 01:08:08 +08:00
2017-02-14 04:12:00 +08:00
// prepare PasteViewer for later preview
2017-02-15 05:21:55 +08:00
PasteViewer . setText ( plainText ) ;
PasteViewer . setFormat ( format ) ;
2018-12-26 00:34:39 +08:00
// prepare cypher message
let file = AttachmentViewer . getAttachmentData ( ) ,
cipherMessage = {
'paste' : plainText
} ;
if ( typeof file !== 'undefined' && file !== null ) {
cipherMessage [ 'attachment' ] = file ;
cipherMessage [ 'attachment_name' ] = AttachmentViewer . getFile ( ) . name ;
} else if ( AttachmentViewer . hasAttachment ( ) ) {
// fall back to cloned part
let attachment = AttachmentViewer . getAttachment ( ) ;
cipherMessage [ 'attachment' ] = attachment [ 0 ] ;
cipherMessage [ 'attachment_name' ] = attachment [ 1 ] ;
2019-08-18 10:17:35 +08:00
// we need to retrieve data from blob if browser already parsed it in memory
if ( typeof attachment [ 0 ] === 'string' && attachment [ 0 ] . startsWith ( 'blob:' ) ) {
Alert . showStatus (
[
'Retrieving cloned file \'%s\' from memory...' ,
attachment [ 1 ]
] ,
'copy'
) ;
try {
const blobData = await $ . ajax ( {
type : 'GET' ,
url : ` ${ attachment [ 0 ] } ` ,
processData : false ,
timeout : 10000 ,
xhrFields : {
withCredentials : false ,
responseType : 'blob'
}
} ) ;
if ( blobData instanceof window . Blob ) {
const fileReading = new Promise ( function ( resolve , reject ) {
const fileReader = new FileReader ( ) ;
fileReader . onload = function ( event ) {
resolve ( event . target . result ) ;
} ;
fileReader . onerror = function ( error ) {
reject ( error ) ;
}
fileReader . readAsDataURL ( blobData ) ;
} ) ;
cipherMessage [ 'attachment' ] = await fileReading ;
} else {
2019-08-27 13:38:27 +08:00
const error = 'Cannot process attachment data.' ;
Alert . showError ( error ) ;
throw new TypeError ( error ) ;
2019-08-18 10:17:35 +08:00
}
} catch ( error ) {
console . error ( error ) ;
2019-08-27 13:38:27 +08:00
Alert . showError ( 'Cannot retrieve attachment.' ) ;
2019-08-18 10:17:35 +08:00
throw error ;
}
}
2018-12-26 00:34:39 +08:00
}
2018-10-09 03:03:10 +08:00
2018-12-26 00:34:39 +08:00
// encrypt message
await ServerInteraction . setCipherMessage ( cipherMessage ) . catch ( Alert . showError ) ;
2018-10-09 03:03:10 +08:00
// send data
2018-12-26 00:34:39 +08:00
ServerInteraction . run ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-15 05:21:55 +08:00
return me ;
} ) ( ) ;
/ * *
* ( controller ) Responsible for decrypting cipherdata and passing data to view .
*
2017-02-18 03:46:10 +08:00
* Only decryption , no download .
*
2017-03-14 03:24:18 +08:00
* @ name PasteDecrypter
2017-02-15 05:21:55 +08:00
* @ class
* /
2018-12-30 01:40:59 +08:00
const PasteDecrypter = ( function ( ) {
const me = { } ;
2017-02-15 05:21:55 +08:00
/ * *
2017-04-11 22:34:13 +08:00
* decrypt data or prompts for password in case of failure
2017-02-15 05:21:55 +08:00
*
2017-03-14 03:24:18 +08:00
* @ name PasteDecrypter . decryptOrPromptPassword
2017-02-15 05:21:55 +08:00
* @ private
2018-10-20 15:56:05 +08:00
* @ async
2017-02-15 05:21:55 +08:00
* @ function
2017-03-14 03:24:18 +08:00
* @ param { string } key
* @ param { string } password - optional , may be an empty string
* @ param { string } cipherdata
2017-02-18 03:46:10 +08:00
* @ throws { string }
2017-03-14 03:24:18 +08:00
* @ return { false | string } false , when unsuccessful or string ( decrypted data )
2017-02-15 05:21:55 +08:00
* /
2018-10-20 15:56:05 +08:00
async function decryptOrPromptPassword ( key , password , cipherdata )
2017-02-15 05:21:55 +08:00
{
// try decryption without password
2018-10-20 23:57:21 +08:00
const plaindata = await CryptTool . decipher ( key , password , cipherdata ) ;
2017-02-15 05:21:55 +08:00
// if it fails, request password
2017-02-18 03:46:10 +08:00
if ( plaindata . length === 0 && password . length === 0 ) {
2017-04-11 22:34:13 +08:00
// show prompt
Prompt . requestPassword ( ) ;
// Thus, we cannot do anything yet, we need to wait for the user
// input.
return false ;
2017-02-15 05:21:55 +08:00
}
2017-02-18 03:46:10 +08:00
// if all tries failed, we can only return an error
if ( plaindata . length === 0 ) {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
return false ;
2017-02-18 03:46:10 +08:00
}
return plaindata ;
}
/ * *
* decrypt the actual paste text
*
2017-04-11 22:34:13 +08:00
* @ name PasteDecrypter . decryptPaste
2017-02-18 03:46:10 +08:00
* @ private
2018-10-20 18:40:08 +08:00
* @ async
2017-02-18 03:46:10 +08:00
* @ function
2019-05-25 19:20:39 +08:00
* @ param { Paste } paste - paste data in object form
2017-03-14 03:24:18 +08:00
* @ param { string } key
* @ param { string } password
2017-02-18 03:46:10 +08:00
* @ throws { string }
2018-10-20 18:40:08 +08:00
* @ return { Promise }
2017-02-18 03:46:10 +08:00
* /
2018-12-30 01:40:59 +08:00
async function decryptPaste ( paste , key , password )
2017-02-18 03:46:10 +08:00
{
2019-05-25 19:20:39 +08:00
let pastePlain = await decryptOrPromptPassword (
2018-12-30 01:40:59 +08:00
key , password ,
2019-05-25 19:20:39 +08:00
paste . getCipherData ( )
2018-12-30 01:40:59 +08:00
) ;
if ( pastePlain === false ) {
if ( password . length === 0 ) {
throw 'waiting on user to provide a password' ;
} else {
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
Alert . hideLoading ( ) ;
// reset password, so it can be re-entered
Prompt . reset ( ) ;
TopNav . showRetryButton ( ) ;
throw 'Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.' ;
2017-02-18 03:46:10 +08:00
}
2018-12-30 01:40:59 +08:00
}
2017-02-15 05:21:55 +08:00
2019-05-25 19:20:39 +08:00
if ( paste . v > 1 ) {
2018-12-30 01:40:59 +08:00
// version 2 paste
const pasteMessage = JSON . parse ( pastePlain ) ;
if ( pasteMessage . hasOwnProperty ( 'attachment' ) && pasteMessage . hasOwnProperty ( 'attachment_name' ) ) {
AttachmentViewer . setAttachment ( pasteMessage . attachment , pasteMessage . attachment _name ) ;
2019-05-19 19:31:17 +08:00
AttachmentViewer . showAttachment ( ) ;
}
2019-05-25 19:20:39 +08:00
pastePlain = pasteMessage . paste ;
2019-05-19 19:31:17 +08:00
} else {
// version 1 paste
if ( paste . hasOwnProperty ( 'attachment' ) && paste . hasOwnProperty ( 'attachmentname' ) ) {
Promise . all ( [
CryptTool . decipher ( key , password , paste . attachment ) ,
CryptTool . decipher ( key , password , paste . attachmentname )
] ) . then ( ( attachment ) => {
AttachmentViewer . setAttachment ( attachment [ 0 ] , attachment [ 1 ] ) ;
AttachmentViewer . showAttachment ( ) ;
} ) ;
2017-02-15 05:21:55 +08:00
}
2018-12-30 01:40:59 +08:00
}
2019-05-25 19:20:39 +08:00
PasteViewer . setFormat ( paste . getFormat ( ) ) ;
PasteViewer . setText ( pastePlain ) ;
2018-12-30 01:40:59 +08:00
PasteViewer . run ( ) ;
2017-02-18 03:46:10 +08:00
}
/ * *
* decrypts all comments and shows them
*
2017-03-14 03:24:18 +08:00
* @ name PasteDecrypter . decryptComments
2017-02-18 03:46:10 +08:00
* @ private
2018-10-20 18:40:08 +08:00
* @ async
2017-02-18 03:46:10 +08:00
* @ function
2019-05-25 19:20:39 +08:00
* @ param { Paste } paste - paste data in object form
2017-03-14 03:24:18 +08:00
* @ param { string } key
* @ param { string } password
2018-10-20 18:40:08 +08:00
* @ return { Promise }
2017-02-18 03:46:10 +08:00
* /
2018-10-20 18:40:08 +08:00
async function decryptComments ( paste , key , password )
2017-02-18 03:46:10 +08:00
{
2018-10-20 16:20:32 +08:00
// remove potential previous discussion
2017-12-18 21:47:17 +08:00
DiscussionViewer . prepareNewDiscussion ( ) ;
2017-02-18 03:46:10 +08:00
2018-12-30 01:40:59 +08:00
const commentDecryptionPromises = [ ] ;
2017-02-18 03:46:10 +08:00
// iterate over comments
2018-10-20 23:57:21 +08:00
for ( let i = 0 ; i < paste . comments . length ; ++ i ) {
2019-05-25 19:20:39 +08:00
const comment = new Comment ( paste . comments [ i ] ) ,
commentPromise = CryptTool . decipher ( key , password , comment . getCipherData ( ) ) ;
paste . comments [ i ] = comment ;
if ( comment . v > 1 ) {
2018-12-30 01:40:59 +08:00
// version 2 comment
commentDecryptionPromises . push (
2019-05-25 19:20:39 +08:00
commentPromise . then ( function ( commentJson ) {
const commentMessage = JSON . parse ( commentJson ) ;
return [
commentMessage . comment || '' ,
commentMessage . nickname || ''
] ;
} )
2018-12-30 01:40:59 +08:00
) ;
} else {
// version 1 comment
commentDecryptionPromises . push (
Promise . all ( [
2019-05-25 19:20:39 +08:00
commentPromise ,
paste . comments [ i ] . meta . hasOwnProperty ( 'nickname' ) ?
2018-12-30 01:40:59 +08:00
CryptTool . decipher ( key , password , paste . comments [ i ] . meta . nickname ) :
Promise . resolve ( '' )
] )
) ;
}
2017-02-18 03:46:10 +08:00
}
2019-05-25 19:20:39 +08:00
return Promise . all ( commentDecryptionPromises ) . then ( function ( plaintexts ) {
2018-10-20 23:57:21 +08:00
for ( let i = 0 ; i < paste . comments . length ; ++ i ) {
2018-10-20 19:54:17 +08:00
if ( plaintexts [ i ] [ 0 ] . length === 0 ) {
2018-10-20 17:40:37 +08:00
continue ;
}
2018-10-20 16:20:32 +08:00
DiscussionViewer . addComment (
2019-05-25 19:20:39 +08:00
paste . comments [ i ] ,
2018-10-20 19:54:17 +08:00
plaintexts [ i ] [ 0 ] ,
plaintexts [ i ] [ 1 ]
2018-10-20 16:20:32 +08:00
) ;
}
} ) ;
2017-02-15 05:21:55 +08:00
}
/ * *
* show decrypted text in the display area , including discussion ( if open )
*
* @ name PasteDecrypter . run
* @ function
2019-05-25 19:20:39 +08:00
* @ param { Paste } [ paste ] - ( optional ) object including comments to display ( items = array with keys ( 'data' , 'meta' ) )
2017-02-15 05:21:55 +08:00
* /
me . run = function ( paste )
{
2017-03-12 23:06:17 +08:00
Alert . hideMessages ( ) ;
2018-01-06 20:32:07 +08:00
Alert . showLoading ( 'Decrypting paste…' , 'cloud-download' ) ;
2017-02-15 05:21:55 +08:00
if ( typeof paste === 'undefined' ) {
2017-04-11 22:34:13 +08:00
// get cipher data and wait until it is available
Model . getPasteData ( me . run ) ;
return ;
2017-02-15 05:21:55 +08:00
}
2018-10-20 17:40:37 +08:00
let key = Model . getPasteKey ( ) ,
password = Prompt . getPassword ( ) ,
2018-10-20 23:57:21 +08:00
decryptionPromises = [ ] ;
2017-02-15 05:21:55 +08:00
2018-10-20 17:40:37 +08:00
TopNav . setRetryCallback ( function ( ) {
TopNav . hideRetryButton ( ) ;
me . run ( paste ) ;
} ) ;
2018-12-30 01:40:59 +08:00
// decrypt paste & attachments
2019-05-19 19:31:17 +08:00
decryptionPromises . push ( decryptPaste ( paste , key , password ) ) ;
2017-03-14 02:30:44 +08:00
2018-10-20 17:40:37 +08:00
// if the discussion is opened on this paste, display it
2019-05-25 19:20:39 +08:00
if ( paste . isDiscussionEnabled ( ) ) {
2018-10-20 23:57:21 +08:00
decryptionPromises . push ( decryptComments ( paste , key , password ) ) ;
2018-10-20 17:40:37 +08:00
}
2017-03-14 02:30:44 +08:00
2018-10-20 19:54:17 +08:00
// shows the remaining time (until) deletion
2019-05-15 13:44:03 +08:00
PasteStatus . showRemainingTime ( paste ) ;
2018-10-20 19:54:17 +08:00
2018-12-30 01:40:59 +08:00
Promise . all ( decryptionPromises )
. then ( ( ) => {
Alert . hideLoading ( ) ;
TopNav . showViewButtons ( ) ;
2019-08-18 10:17:35 +08:00
// discourage cloning (it cannot really be prevented)
if ( paste . isBurnAfterReadingEnabled ( ) ) {
2019-08-22 05:36:22 +08:00
TopNav . hideBurnAfterReadingButtons ( ) ;
} else {
// we have to pass in remaining_time here
TopNav . showEmailButton ( paste . getTimeToLive ( ) ) ;
2019-08-18 10:17:35 +08:00
}
2020-01-09 02:48:42 +08:00
// only offer adding comments, after paste was successfully decrypted
if ( paste . isDiscussionEnabled ( ) ) {
DiscussionViewer . finishDiscussion ( ) ;
}
2018-12-30 01:40:59 +08:00
} )
. catch ( ( err ) => {
// wait for the user to type in the password,
// then PasteDecrypter.run will be called again
making webassembly optional, ensuring retry button works when wrong password is provided
Tested configurations:
- browser with WASM support (Firefox 68.0.2)
- creates paste with zlib compression, no password
- creates paste with zlib compression, with password
- reads paste with zlib compression, no password
- reads paste with zlib compression, with password + retry button works
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
- browser without WASM support (Chromium 76.0.3809.100, started via `chromium-browser --js-flags=--noexpose_wasm`)
- creates paste without compression, no password, but shows WASM warning
- creates paste without compression, with password, but shows WASM warning
- fails to read paste with zlib compression, no password + shows WASM error
- fails to read paste with zlib compression, with password + shows WASM error
- reads paste without compression, no password
- reads paste without compression, with password + retry button works
2019-09-08 14:21:54 +08:00
Alert . showError ( err ) ;
2018-12-30 01:40:59 +08:00
} ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-14 04:12:00 +08:00
2017-02-15 05:21:55 +08:00
return me ;
} ) ( ) ;
2013-11-01 08:15:14 +08:00
2017-01-29 21:32:55 +08:00
/ * *
2017-02-15 05:21:55 +08:00
* ( controller ) main PrivateBin logic
*
2017-03-14 03:24:18 +08:00
* @ name Controller
2017-02-15 05:21:55 +08:00
* @ param { object } window
* @ param { object } document
* @ class
2017-01-29 21:32:55 +08:00
* /
2018-12-30 01:40:59 +08:00
const Controller = ( function ( window , document ) {
const me = { } ;
2017-02-15 05:21:55 +08:00
2017-02-16 05:59:55 +08:00
/ * *
* hides all status messages no matter which module showed them
*
* @ name Controller . hideStatusMessages
* @ function
* /
me . hideStatusMessages = function ( )
{
PasteStatus . hideMessages ( ) ;
Alert . hideMessages ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-16 05:59:55 +08:00
2017-02-13 04:13:04 +08:00
/ * *
* creates a new paste
*
2017-02-15 05:21:55 +08:00
* @ name Controller . newPaste
2017-02-13 04:13:04 +08:00
* @ function
* /
me . newPaste = function ( )
{
2017-02-18 03:46:10 +08:00
// Important: This *must not* run Alert.hideMessages() as previous
// errors from viewing a paste should be shown.
2017-02-18 05:26:39 +08:00
TopNav . hideAllButtons ( ) ;
2018-01-06 20:32:07 +08:00
Alert . showLoading ( 'Preparing new paste…' , 'time' ) ;
2017-02-16 05:59:55 +08:00
2017-02-18 03:46:10 +08:00
PasteStatus . hideMessages ( ) ;
2017-02-15 05:21:55 +08:00
PasteViewer . hide ( ) ;
Editor . resetInput ( ) ;
Editor . show ( ) ;
Editor . focusInput ( ) ;
2017-05-14 03:27:41 +08:00
AttachmentViewer . removeAttachment ( ) ;
2020-04-23 18:07:08 +08:00
TopNav . resetInput ( ) ;
2017-02-16 05:59:55 +08:00
TopNav . showCreateButtons ( ) ;
2019-08-18 10:17:35 +08:00
// newPaste could be called when user is on paste clone editing view
TopNav . hideCustomAttachment ( ) ;
AttachmentViewer . clearDragAndDrop ( ) ;
AttachmentViewer . removeAttachmentData ( ) ;
2017-02-16 05:59:55 +08:00
Alert . hideLoading ( ) ;
2019-08-14 09:44:53 +08:00
history . pushState ( { type : 'create' } , document . title , Helper . baseUri ( ) ) ;
2019-08-15 08:36:44 +08:00
// clear discussion
DiscussionViewer . prepareNewDiscussion ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 04:13:04 +08:00
2017-02-18 03:46:10 +08:00
/ * *
* shows the loaded paste
*
* @ name Controller . showPaste
* @ function
* /
me . showPaste = function ( )
{
try {
Model . getPasteKey ( ) ;
} catch ( err ) {
console . error ( err ) ;
// missing decryption key (or paste ID) in URL?
if ( window . location . hash . length === 0 ) {
2019-08-27 13:38:27 +08:00
Alert . showError ( 'Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)' ) ;
2017-02-18 03:46:10 +08:00
return ;
}
}
// show proper elements on screen
PasteDecrypter . run ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-18 03:46:10 +08:00
/ * *
* refreshes the loaded paste to show potential new data
*
* @ name Controller . refreshPaste
* @ function
2017-03-14 03:24:18 +08:00
* @ param { function } callback
2017-02-18 03:46:10 +08:00
* /
me . refreshPaste = function ( callback )
{
// save window position to restore it later
2018-12-30 01:40:59 +08:00
const orgPosition = $ ( window ) . scrollTop ( ) ;
2017-02-18 03:46:10 +08:00
2017-04-11 22:34:13 +08:00
Model . getPasteData ( function ( data ) {
2018-12-26 00:34:39 +08:00
ServerInteraction . prepare ( ) ;
2019-06-02 05:49:40 +08:00
ServerInteraction . setUrl ( Helper . baseUri ( ) + '?pasteid=' + Model . getPasteId ( ) ) ;
2017-02-18 03:46:10 +08:00
2018-12-26 00:34:39 +08:00
ServerInteraction . setFailure ( function ( status , data ) {
2018-05-01 02:01:38 +08:00
// revert loading status…
Alert . hideLoading ( ) ;
TopNav . showViewButtons ( ) ;
2017-02-18 03:46:10 +08:00
2018-05-01 02:01:38 +08:00
// show error message
Alert . showError (
2018-12-26 00:34:39 +08:00
ServerInteraction . parseUploadError ( status , data , 'refresh display' )
2018-05-01 02:01:38 +08:00
) ;
} ) ;
2018-12-26 00:34:39 +08:00
ServerInteraction . setSuccess ( function ( status , data ) {
2019-05-25 16:10:59 +08:00
PasteDecrypter . run ( new Paste ( data ) ) ;
2017-02-18 03:46:10 +08:00
2018-05-01 02:01:38 +08:00
// restore position
window . scrollTo ( 0 , orgPosition ) ;
2017-04-11 22:34:13 +08:00
2018-05-01 02:01:38 +08:00
// NOTE: could create problems as callback may be called
// asyncronously if PasteDecrypter e.g. needs to wait for a
// password being entered
callback ( ) ;
} ) ;
2018-12-26 00:34:39 +08:00
ServerInteraction . run ( ) ;
2017-04-11 22:34:13 +08:00
} , false ) ; // this false is important as it circumvents the cache
2017-02-25 16:35:55 +08:00
}
2017-02-18 03:46:10 +08:00
2017-02-13 01:08:08 +08:00
/ * *
* clone the current paste
*
2017-02-15 05:21:55 +08:00
* @ name Controller . clonePaste
2017-02-13 01:08:08 +08:00
* @ function
* /
2018-01-06 20:32:07 +08:00
me . clonePaste = function ( )
2017-02-13 01:08:08 +08:00
{
2017-02-16 05:59:55 +08:00
TopNav . collapseBar ( ) ;
2017-02-18 05:26:39 +08:00
TopNav . hideAllButtons ( ) ;
2017-02-16 05:59:55 +08:00
// hide messages from previous paste
me . hideStatusMessages ( ) ;
2017-02-13 01:08:08 +08:00
// erase the id and the key in url
2017-02-16 05:59:55 +08:00
history . pushState ( { type : 'clone' } , document . title , Helper . baseUri ( ) ) ;
2017-02-13 01:08:08 +08:00
2017-02-16 05:59:55 +08:00
if ( AttachmentViewer . hasAttachment ( ) ) {
AttachmentViewer . moveAttachmentTo (
TopNav . getCustomAttachment ( ) ,
'Cloned: \'%s\''
) ;
TopNav . hideFileSelector ( ) ;
AttachmentViewer . hideAttachment ( ) ;
// NOTE: it also looks nice without removing the attachment
// but for a consistent display we remove it…
AttachmentViewer . hideAttachmentPreview ( ) ;
TopNav . showCustomAttachment ( ) ;
// show another status message to make the user aware that the
// file was cloned too!
Alert . showStatus (
[
'The cloned file \'%s\' was attached to this paste.' ,
AttachmentViewer . getAttachment ( ) [ 1 ]
2018-01-06 20:32:07 +08:00
] ,
'copy'
) ;
2017-02-16 05:59:55 +08:00
}
2018-01-06 17:57:54 +08:00
Editor . setText ( PasteViewer . getText ( ) ) ;
2019-08-18 10:17:35 +08:00
// also clone the format
TopNav . setFormat ( PasteViewer . getFormat ( ) ) ;
2017-02-16 05:59:55 +08:00
PasteViewer . hide ( ) ;
Editor . show ( ) ;
TopNav . showCreateButtons ( ) ;
2019-08-15 08:36:44 +08:00
// clear discussion
DiscussionViewer . prepareNewDiscussion ( ) ;
2018-01-06 16:26:10 +08:00
} ;
2017-02-13 01:08:08 +08:00
2019-09-14 15:41:52 +08:00
/ * *
* try initializing zlib or display a warning if it fails ,
* extracted from main init to allow unit testing
*
* @ name Controller . initZ
* @ function
* /
me . initZ = function ( )
{
z = zlib . catch ( function ( ) {
if ( $ ( 'body' ) . data ( 'compression' ) !== 'none' ) {
Alert . showWarning ( 'Your browser doesn\'t support WebAssembly, used for zlib compression. You can create uncompressed documents, but can\'t read compressed ones.' ) ;
}
} ) ;
}
2017-02-13 01:08:08 +08:00
/ * *
* application start
*
2017-02-15 05:21:55 +08:00
* @ name Controller . init
2017-02-13 01:08:08 +08:00
* @ function
* /
2019-09-14 15:41:52 +08:00
me . init = function ( )
2017-02-13 01:08:08 +08:00
{
// first load translations
2017-02-15 05:21:55 +08:00
I18n . loadTranslations ( ) ;
2017-02-13 01:08:08 +08:00
2020-02-02 14:08:38 +08:00
DOMPurify . setConfig ( {
ALLOWED _URI _REGEXP : /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i ,
SAFE _FOR _JQUERY : true
} ) ;
2018-01-01 17:25:07 +08:00
2020-06-01 02:33:22 +08:00
// Add a hook to make all links open a new window
DOMPurify . addHook ( 'afterSanitizeAttributes' , function ( node ) {
// set all elements owning target to target=_blank
if ( 'target' in node && node . id !== 'pasteurl' ) {
node . setAttribute ( 'target' , '_blank' ) ;
}
// set non-HTML/MathML links to xlink:show=new
if ( ! node . hasAttribute ( 'target' )
&& ( node . hasAttribute ( 'xlink:href' )
|| node . hasAttribute ( 'href' ) ) ) {
node . setAttribute ( 'xlink:show' , 'new' ) ;
}
if ( 'rel' in node ) {
node . setAttribute ( 'rel' , 'nofollow noopener noreferrer' ) ;
}
} ) ;
2019-08-22 05:36:22 +08:00
// center all modals
$ ( '.modal' ) . on ( 'show.bs.modal' , function ( e ) {
$ ( e . target ) . css ( {
display : 'flex'
} ) ;
} ) ;
2017-02-13 04:13:04 +08:00
// initialize other modules/"classes"
2017-02-15 05:21:55 +08:00
Alert . init ( ) ;
2017-02-16 05:59:55 +08:00
Model . init ( ) ;
2017-02-18 05:46:18 +08:00
AttachmentViewer . init ( ) ;
DiscussionViewer . init ( ) ;
2017-02-15 05:21:55 +08:00
Editor . init ( ) ;
PasteStatus . init ( ) ;
PasteViewer . init ( ) ;
Prompt . init ( ) ;
2017-02-18 05:46:18 +08:00
TopNav . init ( ) ;
UiHelper . init ( ) ;
2019-09-14 15:41:52 +08:00
// check for legacy browsers before going any further
2019-09-18 13:31:32 +08:00
if ( ! Legacy . Check . getInit ( ) ) {
2019-09-14 15:41:52 +08:00
// Legacy check didn't complete, wait and try again
setTimeout ( init , 500 ) ;
return ;
}
2019-09-18 13:31:32 +08:00
if ( ! Legacy . Check . getStatus ( ) ) {
2019-06-28 03:18:46 +08:00
// something major is wrong, stop right away
return ;
}
2019-09-14 15:41:52 +08:00
me . initZ ( ) ;
2017-02-09 03:11:04 +08:00
2020-05-30 18:00:17 +08:00
// if delete token is passed (i.e. paste has been deleted by this
// access), there is nothing more to do
if ( Model . hasDeleteToken ( ) ) {
return ;
}
2017-04-11 22:34:13 +08:00
// check whether existing paste needs to be shown
try {
Model . getPasteId ( ) ;
} catch ( e ) {
// otherwise create a new paste
return me . newPaste ( ) ;
}
2020-05-30 17:57:27 +08:00
// always reload on back button to invalidate cache(protect burn after read paste)
window . addEventListener ( 'popstate' , ( ) => {
window . location . reload ( ) ;
} ) ;
2018-05-22 17:41:35 +08:00
2018-05-01 02:01:38 +08:00
// display an existing paste
2017-04-11 22:34:13 +08:00
return me . showPaste ( ) ;
2017-02-25 16:35:55 +08:00
}
2017-02-09 03:12:22 +08:00
return me ;
} ) ( window , document ) ;
2017-01-29 21:32:55 +08:00
2017-01-22 17:42:11 +08:00
return {
2017-02-15 05:21:55 +08:00
Helper : Helper ,
I18n : I18n ,
CryptTool : CryptTool ,
2017-02-18 05:46:18 +08:00
Model : Model ,
UiHelper : UiHelper ,
2017-02-15 05:21:55 +08:00
Alert : Alert ,
2017-02-18 05:46:18 +08:00
PasteStatus : PasteStatus ,
Prompt : Prompt ,
Editor : Editor ,
2017-02-18 03:46:10 +08:00
PasteViewer : PasteViewer ,
2017-02-18 05:46:18 +08:00
AttachmentViewer : AttachmentViewer ,
DiscussionViewer : DiscussionViewer ,
TopNav : TopNav ,
2018-12-26 00:34:39 +08:00
ServerInteraction : ServerInteraction ,
2017-02-18 05:46:18 +08:00
PasteEncrypter : PasteEncrypter ,
PasteDecrypter : PasteDecrypter ,
Controller : Controller
2017-01-22 17:42:11 +08:00
} ;
2018-10-21 01:53:21 +08:00
} ) ( jQuery , RawDeflate ) ;