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