2012-04-22 12:45:45 +02:00
/ * *
2016-07-11 11:58:15 +02:00
* PrivateBin
2012-04-23 16:30:02 +02:00
*
2012-04-30 22:58:08 +02:00
* a zero - knowledge paste bin
*
2017-01-14 15:29:12 +01:00
* @ see { @ link https : //github.com/PrivateBin/PrivateBin}
* @ copyright 2012 Sébastien SAUVAGE ( { @ link http : //sebsauvage.net})
* @ license { @ link https : //www.opensource.org/licenses/zlib-license.php The zlib/libpng License}
2016-12-26 12:13:50 +01:00
* @ version 1.1
2017-01-14 15:29:12 +01:00
* @ name PrivateBin
* @ namespace
2012-04-22 12:45:45 +02:00
* /
2012-04-21 21:59:45 +02:00
2015-09-06 13:07:46 +02:00
'use strict' ;
2016-07-11 15:47:42 +02:00
/** global: Base64 */
/** global: FileReader */
/** global: RawDeflate */
/** global: history */
/** global: navigator */
/** global: prettyPrint */
/** global: prettyPrintOne */
/** global: showdown */
/** global: sjcl */
2015-09-06 13:07:46 +02:00
2012-04-21 21:59:45 +02:00
// Immediately start random number generator collector.
sjcl . random . startCollectors ( ) ;
2017-02-08 20:11:04 +01:00
// startup
jQuery ( document ) . ready ( function ( ) {
/ * *
* main application start , called when DOM is fully loaded and
* runs controller initalization after translations are loaded
* /
PrivateBin . i18n . loadTranslations ( ) ;
} ) ;
/ * *
* @ name PrivateBin
* @ namespace
* /
var PrivateBin = window . PrivateBin || { } ;
/ * *
* static helper methods
*
* @ param { object } window
* @ param { object } document
* @ name helper
* @ class
* /
PrivateBin . helper = ( function ( window , document , jQuery , sjcl , Base64 , RawDeflate ) {
var me = { } ;
2017-02-08 13:20:51 +01:00
2015-09-05 17:12:11 +02:00
/ * *
2017-02-08 20:11:04 +01:00
* character to HTML entity lookup table
2017-01-14 15:29:12 +01:00
*
2017-02-08 20:11:04 +01:00
* @ see { @ link https : //github.com/janl/mustache.js/blob/master/mustache.js#L60}
* @ private
* @ enum { Object }
* @ readonly
2015-09-05 17:12:11 +02:00
* /
2017-02-08 20:11:04 +01:00
var entityMap = {
'&' : '&' ,
'<' : '<' ,
'>' : '>' ,
'"' : '"' ,
"'" : ''' ,
'/' : '/' ,
'`' : '`' ,
'=' : '='
} ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* cache for script location
*
* @ private
* @ enum { string | null }
* /
var scriptLocation = null ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* converts a duration ( in seconds ) into human friendly approximation
*
* @ name helper . secondsToHuman
* @ function
* @ param { number } seconds
* @ return { Array }
* /
me . secondsToHuman = function ( seconds )
{
var v ;
if ( seconds < 60 )
{
v = Math . floor ( seconds ) ;
return [ v , 'second' ] ;
}
if ( seconds < 60 * 60 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
v = Math . floor ( seconds / 60 ) ;
return [ v , 'minute' ] ;
}
if ( seconds < 60 * 60 * 24 )
{
v = Math . floor ( seconds / ( 60 * 60 ) ) ;
return [ v , 'hour' ] ;
}
// If less than 2 months, display in days:
if ( seconds < 60 * 60 * 24 * 60 )
{
v = Math . floor ( seconds / ( 60 * 60 * 24 ) ) ;
return [ v , 'day' ] ;
}
v = Math . floor ( seconds / ( 60 * 60 * 24 * 30 ) ) ;
return [ v , 'month' ] ;
} ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +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 )
{
var range , selection ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// MS
if ( document . body . createTextRange )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
range = document . body . createTextRange ( ) ;
range . moveToElementText ( element ) ;
range . select ( ) ;
}
// all others
else if ( window . getSelection )
2015-09-12 17:33:16 +02:00
{
2017-02-08 20:11:04 +01:00
selection = window . getSelection ( ) ;
range = document . createRange ( ) ;
range . selectNodeContents ( element ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
}
} ;
2015-09-12 17:33:16 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* set text of a jQuery element ( required for IE ) ,
*
* @ name helper . setElementText
* @ function
* @ param { jQuery } $element - a jQuery element
* @ param { string } text - the text to enter
* @ TODO check for XSS attacks , usually no CSS can prevent them so this looks weird on the first look
* /
me . setElementText = function ( $element , text )
{
// For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
if ( $ ( '#oldienotice' ) . is ( ':visible' ) ) {
var html = me . htmlEntities ( text ) . replace ( /\n/ig , '\r\n<br>' ) ;
$element . html ( '<pre>' + html + '</pre>' ) ;
}
// for other (sane) browsers:
else
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
$element . text ( text ) ;
}
} ;
2015-09-06 13:07:46 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* replace last child of element with message
*
* @ name helper . setMessage
* @ function
* @ param { jQuery } $element - a jQuery wrapped DOM element
* @ param { string } message - the message to append
* /
me . setMessage = function ( $element , message )
{
var content = $element . contents ( ) ;
if ( content . length > 0 )
2015-09-06 13:07:46 +02:00
{
2017-02-08 20:11:04 +01:00
content [ content . length - 1 ] . nodeValue = ' ' + message ;
}
else
{
me . setElementText ( $element , message ) ;
}
} ;
2015-09-21 22:43:00 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* convert URLs to clickable links .
* URLs to handle :
* < pre >
* magnet : ? xt . 1 = urn : sha1 : YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C & xt . 2 = urn : sha1 : TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
* http : //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
* @ param { Object } element - a jQuery DOM element
* /
me . urls2links = function ( element )
{
var markup = '<a href="$1" rel="nofollow">$1</a>' ;
element . html (
element . html ( ) . replace (
/((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig ,
markup
)
) ;
element . html (
element . html ( ) . replace (
/((magnet):[\w?=&.\/-;#@~%+-]+)/ig ,
markup
)
) ;
} ;
/ * *
* minimal sprintf emulation for % s and % d formats
*
* @ 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 ( )
{
var args = arguments ;
if ( typeof arguments [ 0 ] === 'object' )
{
args = arguments [ 0 ] ;
}
var format = args [ 0 ] ,
i = 1 ;
return format . replace ( /%((%)|s|d)/g , function ( m ) {
// m is the matched format, e.g. %s, %d
var val ;
if ( m [ 2 ] ) {
val = m [ 2 ] ;
} else {
val = args [ i ] ;
// A switch statement so that the formatter can be extended.
switch ( m )
2016-07-11 15:47:42 +02:00
{
2017-02-08 20:11:04 +01:00
case '%d' :
val = parseFloat ( val ) ;
if ( isNaN ( val ) ) {
val = 0 ;
}
break ;
default :
// Default is %s
2016-07-11 15:47:42 +02:00
}
2017-02-08 20:11:04 +01:00
++ i ;
2015-09-19 11:21:13 +02:00
}
2017-02-08 20:11:04 +01:00
return val ;
} ) ;
} ;
2016-07-19 16:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* 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
* @ return { string }
* /
me . getCookie = function ( cname ) {
var name = cname + '=' ,
ca = document . cookie . split ( ';' ) ;
for ( var i = 0 ; i < ca . length ; ++ i ) {
var c = ca [ i ] ;
while ( c . charAt ( 0 ) === ' ' )
{
c = c . substring ( 1 ) ;
2017-02-08 13:20:51 +01:00
}
2017-02-08 20:11:04 +01:00
if ( c . indexOf ( name ) === 0 )
2017-02-05 14:47:03 +01:00
{
2017-02-08 20:11:04 +01:00
return c . substring ( name . length , c . length ) ;
2017-02-05 14:47:03 +01:00
}
2017-02-08 20:11:04 +01:00
}
return '' ;
} ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* get the current script location ( without search or hash part of the URL ) ,
* eg . http : //example.com/path/?aaaa#bbbb --> http://example.com/path/
*
* @ name helper . scriptLocation
* @ function
* @ return { string } current script location
* /
me . scriptLocation = function ( )
{
// check for cached version
if ( scriptLocation !== null ) {
2017-02-05 14:47:03 +01:00
return scriptLocation ;
2017-02-08 20:11:04 +01:00
}
scriptLocation = window . location . href . substring (
0 ,
window . location . href . length - window . location . search . length - window . location . hash . length
) ;
2017-02-05 14:47:03 +01:00
2017-02-08 20:11:04 +01:00
var hashIndex = scriptLocation . indexOf ( '?' ) ;
if ( hashIndex !== - 1 )
2017-02-05 14:47:03 +01:00
{
2017-02-08 20:11:04 +01:00
scriptLocation = scriptLocation . substring ( 0 , hashIndex ) ;
}
return scriptLocation ;
} ;
/ * *
* get the pastes unique identifier from the URL ,
* eg . http : //example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
*
* @ name helper . pasteId
* @ function
* @ return { string } unique identifier
* /
me . pasteId = function ( )
{
return window . location . search . substring ( 1 ) ;
} ;
2017-02-05 14:47:03 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* return the deciphering key stored in anchor part of the URL
*
* @ name helper . pageKey
* @ function
* @ return { string } key
* /
me . pageKey = function ( )
{
var key = window . location . hash . substring ( 1 ) ,
i = key . indexOf ( '&' ) ;
// Some web 2.0 services and redirectors add data AFTER the anchor
// (such as &utm_source=...). We will strip any additional data.
if ( i > - 1 )
2017-02-05 14:47:03 +01:00
{
2017-02-08 20:11:04 +01:00
key = key . substring ( 0 , i ) ;
}
2017-02-05 18:03:42 +01:00
2017-02-08 20:11:04 +01:00
return key ;
} ;
2017-02-05 14:47:03 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* convert all applicable characters to HTML entities
*
* @ see { @ link https : //www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
* @ name helper . htmlEntities
* @ function
* @ param { string } str
* @ return { string } escaped HTML
* /
me . htmlEntities = function ( str ) {
return String ( str ) . replace (
/[&<>"'`=\/]/g , function ( s ) {
return entityMap [ s ] ;
} ) ;
} ;
2017-02-05 14:47:03 +01:00
2017-02-08 20:11:04 +01:00
return me ;
} ) ( window , document , jQuery , sjcl , Base64 , RawDeflate ) ;
2016-07-19 16:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* internationalization methods
*
* @ param { object } window
* @ param { object } document
* @ name i18n
* @ class
* /
PrivateBin . i18n = ( function ( window , document , jQuery , sjcl , Base64 , RawDeflate ) {
var me = { } ;
2015-09-05 17:12:11 +02:00
2015-09-06 13:07:46 +02:00
/ * *
2017-02-08 20:11:04 +01:00
* supported languages , minus the built in 'en'
2017-01-14 15:29:12 +01:00
*
2017-02-08 20:11:04 +01:00
* @ private
* @ prop { string [ ] }
* @ readonly
2015-09-06 13:07:46 +02:00
* /
2017-02-08 20:11:04 +01:00
var supportedLanguages = [ 'de' , 'es' , 'fr' , 'it' , 'no' , 'pl' , 'oc' , 'ru' , 'sl' , 'zh' ] ;
/ * *
* built in language
*
* @ private
* @ prop { string }
* /
var language = 'en' ;
/ * *
* translation cache
*
* @ private
* @ enum { Object }
* /
var translations = { } ;
/ * *
* translate a string , alias for i18n . translate ( )
*
* @ name i18n . _
* @ function
* @ param { string } messageId
* @ param { ... * } args - one or multiple parameters injected into placeholders
* @ return { string }
* /
me . _ = function ( )
{
return me . translate ( arguments ) ;
} ;
2015-09-06 13:07:46 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* translate a string
*
* @ name i18n . translate
* @ function
* @ param { string } messageId
* @ param { ... * } args - one or multiple parameters injected into placeholders
* @ return { string }
* /
me . translate = function ( )
{
var args = arguments , messageId ;
if ( typeof arguments [ 0 ] === 'object' )
2015-09-06 13:07:46 +02:00
{
2017-02-08 20:11:04 +01:00
args = arguments [ 0 ] ;
}
var 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 ( ! translations . hasOwnProperty ( messageId ) )
{
if ( language !== 'en' )
2015-09-06 15:54:43 +02:00
{
2017-02-08 20:11:04 +01:00
console . error (
'Missing ' + language + ' translation for: ' + messageId
) ;
2015-09-06 15:54:43 +02:00
}
2017-02-08 20:11:04 +01:00
translations [ messageId ] = args [ 0 ] ;
}
if ( usesPlurals && $ . isArray ( translations [ messageId ] ) )
{
var n = parseInt ( args [ 1 ] || 1 , 10 ) ,
key = me . getPluralForm ( n ) ,
maxKey = translations [ messageId ] . length - 1 ;
if ( key > maxKey )
2015-09-06 15:54:43 +02:00
{
2017-02-08 20:11:04 +01:00
key = maxKey ;
2015-09-06 13:07:46 +02:00
}
2017-02-08 20:11:04 +01:00
args [ 0 ] = translations [ messageId ] [ key ] ;
args [ 1 ] = n ;
}
else
{
args [ 0 ] = translations [ messageId ] ;
}
return helper . sprintf ( args ) ;
} ;
2015-09-06 13:07:46 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* per language functions to use to determine the plural form
*
* @ see { @ link http : //localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
* @ name i18n . getPluralForm
* @ function
* @ param { number } n
* @ return { number } array key
* /
me . getPluralForm = function ( n ) {
switch ( language )
{
case 'fr' :
case 'oc' :
case 'zh' :
return ( n > 1 ? 1 : 0 ) ;
case 'pl' :
return ( n === 1 ? 0 : ( n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ) ) ;
case 'ru' :
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 ) ) ) ;
// de, en, es, it, no
default :
return ( n !== 1 ? 1 : 0 ) ;
}
} ;
/ * *
* load translations into cache , then trigger controller initialization
*
* @ name i18n . loadTranslations
* @ function
* /
me . loadTranslations = function ( )
{
var newLanguage = PrivateBin . helper . getCookie ( 'lang' ) ;
2015-09-08 20:48:18 +02:00
2017-02-08 20:11:04 +01:00
// auto-select language based on browser settings
if ( newLanguage . length === 0 )
2015-09-06 13:07:46 +02:00
{
2017-02-08 20:11:04 +01:00
newLanguage = ( navigator . language || navigator . userLanguage ) . substring ( 0 , 2 ) ;
}
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
// if language is already used (e.g, default 'en'), skip update
if ( newLanguage === language )
{
controller . init ( ) ;
return ;
}
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
// 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 ) ;
controller . init ( ) ;
}
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
// load strongs from JSON
$ . getJSON ( 'i18n/' + newLanguage + '.json' , function ( data ) {
language = newLanguage ;
translations = data ;
} ) . fail ( function ( data , textStatus , errorMsg ) {
console . error ( 'Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.' , newLanguage , textStatus , errorMsg ) ;
} ) ;
2015-09-06 13:07:46 +02:00
2017-02-08 20:11:04 +01:00
controller . init ( ) ;
} ;
2015-09-06 15:54:43 +02:00
2017-02-08 20:11:04 +01:00
return me ;
} ) ( window , document , jQuery , sjcl , Base64 , RawDeflate ) ;
/ * *
* filter methods
*
* @ param { object } window
* @ param { object } document
* @ name filter
* @ class
* /
PrivateBin . filter = ( function ( window , document , jQuery , sjcl , Base64 , RawDeflate ) {
var me = { } ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* compress a message ( deflate compression ) , returns base64 encoded data
*
* @ name filter . compress
* @ function
* @ param { string } message
* @ return { string } base64 data
* /
me . compress = function ( message )
{
return Base64 . toBase64 ( RawDeflate . deflate ( Base64 . utob ( message ) ) ) ;
} ;
2015-09-06 13:07:46 +02:00
2015-09-05 17:12:11 +02:00
/ * *
2017-02-08 20:11:04 +01:00
* decompress a message compressed with filter . compress ( )
2017-01-14 15:29:12 +01:00
*
2017-02-08 20:11:04 +01:00
* @ name filter . decompress
* @ function
* @ param { string } data - base64 data
* @ return { string } message
2015-09-05 17:12:11 +02:00
* /
2017-02-08 20:11:04 +01:00
me . decompress = function ( data )
{
return Base64 . btou ( RawDeflate . inflate ( Base64 . fromBase64 ( data ) ) ) ;
} ;
/ * *
* compress , then encrypt message with given key and password
*
* @ name filter . cipher
* @ function
* @ param { string } key
* @ param { string } password
* @ param { string } message
* @ return { string } data - JSON with encrypted data
* /
me . cipher = function ( key , password , message )
{
// Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
var options = { mode : 'gcm' , ks : 256 , ts : 128 } ;
if ( ( password || '' ) . trim ( ) . length === 0 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
return sjcl . encrypt ( key , me . compress ( message ) , options ) ;
}
return sjcl . encrypt ( key + sjcl . codec . hex . fromBits ( sjcl . hash . sha256 . hash ( password ) ) , me . compress ( message ) , options ) ;
} ;
/ * *
* decrypt message with key , then decompress
*
* @ name filter . decipher
* @ function
* @ param { string } key
* @ param { string } password
* @ param { string } data - JSON with encrypted data
* @ return { string } decrypted message
* /
me . decipher = function ( key , password , data )
{
if ( data !== undefined )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
try
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
return me . decompress ( sjcl . decrypt ( key , data ) ) ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
catch ( err )
2015-09-05 17:12:11 +02:00
{
try
{
2017-02-08 20:11:04 +01:00
return me . decompress ( sjcl . decrypt ( key + sjcl . codec . hex . fromBits ( sjcl . hash . sha256 . hash ( password ) ) , data ) ) ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
catch ( e )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
// ignore error, because ????? @TODO
2015-08-31 21:14:12 +02:00
}
}
2015-08-22 17:23:41 +02:00
}
2017-02-08 20:11:04 +01:00
return '' ;
} ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
return me ;
} ) ( window , document , jQuery , sjcl , Base64 , RawDeflate ) ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* PrivateBin logic
*
* @ param { object } window
* @ param { object } document
* @ name controller
* @ class
* /
PrivateBin . controller = ( function ( window , document , jQuery , sjcl , Base64 , RawDeflate ) {
var me = { } ;
/ * *
* headers to send in AJAX requests
2017-01-14 15:29:12 +01:00
*
2017-02-08 20:11:04 +01:00
* @ private
* @ enum { Object }
2017-01-14 15:29:12 +01:00
* /
2017-02-08 20:11:04 +01:00
var headers = { 'X-Requested-With' : 'JSONHttpRequest' } ;
/ * *
* URL shortners create address
*
* @ private
* @ prop { string }
* /
var shortenerUrl = '' ;
/ * *
* URL of newly created paste
*
* @ private
* @ prop { string }
* /
var createdPasteUrl = '' ;
// jQuery pre-loaded objects
var $attach ,
$attachment ,
$attachmentLink ,
$burnAfterReading ,
$burnAfterReadingOption ,
$cipherData ,
$clearText ,
$cloneButton ,
$clonedFile ,
$comments ,
$discussion ,
$errorMessage ,
$expiration ,
$fileRemoveButton ,
$fileWrap ,
$formatter ,
$image ,
$loadingIndicator ,
$message ,
$messageEdit ,
$messagePreview ,
$newButton ,
$openDisc , // @TODO: rename - too similar to openDiscussion, difference unclear
$openDiscussion ,
$password ,
$passwordInput ,
$passwordModal ,
$passwordForm ,
$passwordDecrypt ,
$pasteResult ,
$pasteUrl ,
$prettyMessage ,
$prettyPrint ,
$preview ,
$rawTextButton ,
$remainingTime ,
$replyStatus ,
$sendButton ,
$status ;
/ * *
* ask the user for the password and set it
*
* @ name controller . requestPassword
* @ function
* /
me . requestPassword = function ( )
{
if ( $passwordModal . length === 0 ) {
var password = prompt ( i18n . _ ( 'Please enter the password for this paste:' ) , '' ) ;
if ( password === null )
{
throw 'password prompt canceled' ;
}
if ( password . length === 0 )
{
// recursive…
me . requestPassword ( ) ;
2016-11-13 18:12:10 +01:00
} else {
2017-02-08 20:11:04 +01:00
$passwordInput . val ( password ) ;
me . displayMessages ( ) ;
2016-11-13 18:12:10 +01:00
}
2017-02-08 20:11:04 +01:00
} else {
$passwordModal . modal ( ) ;
}
} ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* use given format on paste , defaults to plain text
*
* @ name controller . formatPaste
* @ function
* @ param { string } format
* @ param { string } text
* /
me . formatPaste = function ( format , text )
{
helper . setElementText ( $clearText , text ) ;
helper . setElementText ( $prettyPrint , text ) ;
switch ( format || 'plaintext' ) {
case 'markdown' :
// silently fail if showdown is not available
// @TODO: maybe better show an error message? At least a warning?
if ( typeof showdown === 'object' )
{
var converter = new showdown . Converter ( {
strikethrough : true ,
tables : true ,
tablesHeaderId : true
} ) ;
$clearText . html (
converter . makeHtml ( text )
) ;
// add table classes from bootstrap css
$clearText . find ( 'table' ) . addClass ( 'table-condensed table-bordered' ) ;
2016-12-12 17:37:51 +01:00
2017-02-08 20:11:04 +01:00
$clearText . removeClass ( 'hidden' ) ;
} else {
console . error ( 'showdown is not loaded, could not parse Markdown' ) ;
}
$prettyMessage . addClass ( 'hidden' ) ;
break ;
case 'syntaxhighlighting' :
// silently fail if prettyprint is not available
// @TODO: maybe better show an error message? At least a warning?
if ( typeof prettyPrintOne === 'function' )
{
if ( typeof prettyPrint === 'function' )
2016-07-11 11:09:41 +02:00
{
2017-02-08 20:11:04 +01:00
prettyPrint ( ) ;
2016-07-11 14:15:20 +02:00
}
2017-02-08 20:11:04 +01:00
$prettyPrint . html (
prettyPrintOne (
helper . htmlEntities ( text ) , null , true
)
) ;
} else {
console . error ( 'pretty print is not loaded, could not link ' ) ;
}
// fall through, as the rest is the same
default : // = 'plaintext'
// convert URLs to clickable links
helper . urls2links ( $clearText ) ;
helper . urls2links ( $prettyPrint ) ;
$clearText . addClass ( 'hidden' ) ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
$prettyPrint . css ( 'white-space' , 'pre-wrap' ) ;
$prettyPrint . css ( 'word-break' , 'normal' ) ;
$prettyPrint . removeClass ( 'prettyprint' ) ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
$prettyMessage . removeClass ( 'hidden' ) ;
}
} ;
2015-09-12 17:33:16 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* show decrypted text in the display area , including discussion ( if open )
*
* @ name controller . displayMessages
* @ function
* @ param { Object } [ paste ] - ( optional ) object including comments to display ( items = array with keys ( 'data' , 'meta' ) )
* /
me . displayMessages = function ( paste )
{
paste = paste || $ . parseJSON ( $cipherData . text ( ) ) ;
var key = helper . pageKey ( ) ,
password = $passwordInput . val ( ) ;
if ( ! $prettyPrint . hasClass ( 'prettyprinted' ) ) {
// Try to decrypt the paste.
try
{
if ( paste . attachment )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
var attachment = filter . decipher ( key , password , paste . attachment ) ;
if ( attachment . length === 0 )
2015-09-16 22:51:48 +02:00
{
2017-02-08 20:11:04 +01:00
if ( password . length === 0 )
2015-09-18 21:41:50 +02:00
{
2017-02-08 20:11:04 +01:00
me . requestPassword ( ) ;
return ;
2015-09-18 21:41:50 +02:00
}
2017-02-08 20:11:04 +01:00
attachment = filter . decipher ( key , password , paste . attachment ) ;
2015-09-16 22:51:48 +02:00
}
2017-02-08 20:11:04 +01:00
if ( attachment . length === 0 )
2015-09-18 12:33:10 +02:00
{
2017-02-08 20:11:04 +01:00
throw 'failed to decipher attachment' ;
2015-09-18 12:33:10 +02:00
}
2017-02-08 20:11:04 +01:00
if ( paste . attachmentname )
2016-07-11 15:47:42 +02:00
{
2017-02-08 20:11:04 +01:00
var attachmentname = filter . decipher ( key , password , paste . attachmentname ) ;
if ( attachmentname . length > 0 )
{
$attachmentLink . attr ( 'download' , attachmentname ) ;
}
2016-07-11 15:47:42 +02:00
}
2017-02-08 20:11:04 +01:00
$attachmentLink . attr ( 'href' , attachment ) ;
$attachment . removeClass ( 'hidden' ) ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// if the attachment is an image, display it
var imagePrefix = 'data:image/' ;
if ( attachment . substring ( 0 , imagePrefix . length ) === imagePrefix )
2015-09-18 21:41:50 +02:00
{
2017-02-08 20:11:04 +01:00
$image . html (
$ ( document . createElement ( 'img' ) )
. attr ( 'src' , attachment )
. attr ( 'class' , 'img-thumbnail' )
) ;
$image . removeClass ( 'hidden' ) ;
2015-09-18 21:41:50 +02:00
}
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
var cleartext = filter . decipher ( key , password , paste . data ) ;
if ( cleartext . length === 0 && password . length === 0 && ! paste . attachment )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
me . requestPassword ( ) ;
2015-09-05 17:12:11 +02:00
return ;
}
2017-02-08 20:11:04 +01:00
if ( cleartext . length === 0 && ! paste . attachment )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
throw 'failed to decipher message' ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 14:15:58 +01:00
2017-02-08 20:11:04 +01:00
$passwordInput . val ( password ) ;
if ( cleartext . length > 0 )
{
$ ( '#pasteFormatter' ) . val ( paste . meta . formatter ) ;
me . formatPaste ( paste . meta . formatter , cleartext ) ;
}
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
catch ( err )
2016-07-11 15:47:42 +02:00
{
2017-02-08 20:11:04 +01:00
me . stateOnlyNewPaste ( ) ;
me . showError ( i18n . _ ( 'Could not decrypt data (Wrong key?)' ) ) ;
2016-07-11 15:47:42 +02:00
return ;
}
2017-02-08 20:11:04 +01:00
}
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// display paste expiration / for your eyes only
if ( paste . meta . expire _date )
{
var expiration = helper . secondsToHuman ( paste . meta . remaining _time ) ,
expirationLabel = [
'This document will expire in %d ' + expiration [ 1 ] + '.' ,
'This document will expire in %d ' + expiration [ 1 ] + 's.'
] ;
helper . setMessage ( $remainingTime , i18n . _ ( expirationLabel , expiration [ 0 ] ) ) ;
$remainingTime . removeClass ( 'foryoureyesonly' )
. removeClass ( 'hidden' ) ;
}
if ( paste . meta . burnafterreading )
{
// unfortunately many web servers don't support DELETE (and PUT) out of the box
2015-09-27 20:34:39 +02:00
$ . ajax ( {
type : 'POST' ,
2017-02-08 20:11:04 +01:00
url : helper . scriptLocation ( ) + '?' + helper . pasteId ( ) ,
data : { deletetoken : 'burnafterreading' } ,
2015-09-27 20:34:39 +02:00
dataType : 'json' ,
2017-02-08 20:11:04 +01:00
headers : headers
2015-09-27 20:34:39 +02:00
} )
2015-09-01 22:33:07 +02:00
. fail ( function ( ) {
2017-02-08 20:11:04 +01:00
controller . showError ( i18n . _ ( 'Could not delete the paste, it was not stored in burn after reading mode.' ) ) ;
2015-09-01 22:33:07 +02:00
} ) ;
2017-02-08 20:11:04 +01:00
helper . setMessage ( $remainingTime , i18n . _ (
'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
) ) ;
$remainingTime . addClass ( 'foryoureyesonly' )
. removeClass ( 'hidden' ) ;
// discourage cloning (as it can't really be prevented)
$cloneButton . addClass ( 'hidden' ) ;
}
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// if the discussion is opened on this paste, display it
if ( paste . meta . opendiscussion )
2015-09-01 22:33:07 +02:00
{
2017-02-08 20:11:04 +01:00
$comments . html ( '' ) ;
2013-02-24 16:29:13 +01:00
2017-02-08 20:11:04 +01:00
var $divComment ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// iterate over comments
for ( var i = 0 ; i < paste . comments . length ; ++ i )
2015-09-16 22:51:48 +02:00
{
2017-02-08 20:11:04 +01:00
var $place = $comments ,
comment = paste . comments [ i ] ,
commentText = filter . decipher ( key , password , comment . data ) ,
$parentComment = $ ( '#comment_' + comment . parentid ) ;
$divComment = $ ( '<article><div class="comment" id="comment_' + comment . id
+ '"><div class="commentmeta"><span class="nickname"></span>'
+ '<span class="commentdate"></span></div>'
+ '<div class="commentdata"></div>'
+ '<button class="btn btn-default btn-sm">'
+ i18n . _ ( 'Reply' ) + '</button></div></article>' ) ;
var $divCommentData = $divComment . find ( 'div.commentdata' ) ;
// if parent comment exists
if ( $parentComment . length )
2015-09-16 22:51:48 +02:00
{
2017-02-08 20:11:04 +01:00
// shift comment to the right
$place = $parentComment ;
2015-09-16 22:51:48 +02:00
}
2017-02-08 20:11:04 +01:00
$divComment . find ( 'button' ) . click ( { commentid : comment . id } , me . openReply ) ;
helper . setElementText ( $divCommentData , commentText ) ;
helper . urls2links ( $divCommentData ) ;
2015-09-16 22:51:48 +02:00
2017-02-08 20:11:04 +01:00
// try to get optional nickname
var nick = filter . decipher ( key , password , comment . meta . nickname ) ;
if ( nick . length > 0 )
2015-09-18 12:33:10 +02:00
{
2017-02-08 20:11:04 +01:00
$divComment . find ( 'span.nickname' ) . text ( nick ) ;
2015-09-18 12:33:10 +02:00
}
2017-02-08 20:11:04 +01:00
else
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
divComment . find ( 'span.nickname' ) . html ( '<i>' + i18n . _ ( 'Anonymous' ) + '</i>' ) ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
$divComment . find ( 'span.commentdate' )
. text ( ' (' + ( new Date ( comment . meta . postdate * 1000 ) . toLocaleString ( ) ) + ')' )
. attr ( 'title' , 'CommentID: ' + comment . id ) ;
2012-04-21 21:59:45 +02:00
2017-02-08 20:11:04 +01:00
// if an avatar is available, display it
if ( comment . meta . vizhash )
{
$divComment . find ( 'span.nickname' )
. before (
'<img src="' + comment . meta . vizhash + '" class="vizhash" title="' +
i18n . _ ( 'Anonymous avatar (Vizhash of the IP address)' ) + '" /> '
) ;
}
2016-01-31 09:56:06 +01:00
2017-02-08 20:11:04 +01:00
$place . append ( $divComment ) ;
}
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// add 'add new comment' area
$divComment = $ (
'<div class="comment"><button class="btn btn-default btn-sm">' +
i18n . _ ( 'Add comment' ) + '</button></div>'
) ;
$divComment . find ( 'button' ) . click ( { commentid : helper . pasteId ( ) } , me . openReply ) ;
$comments . append ( $divComment ) ;
$discussion . removeClass ( 'hidden' ) ;
}
} ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* open the comment entry when clicking the "Reply" button of a comment
*
* @ name controller . openReply
* @ function
* @ param { Event } event
* /
me . openReply = function ( event )
{
event . preventDefault ( ) ;
// remove any other reply area
$ ( 'div.reply' ) . remove ( ) ;
var source = $ ( event . target ) ,
commentid = event . data . commentid ,
hint = i18n . _ ( 'Optional nickname...' ) ,
reply = $ (
'<div class="reply"><input type="text" id="nickname" ' +
'class="form-control" title="' + hint + '" placeholder="' +
hint + '" /><textarea id="replymessage" class="replymessage ' +
'form-control" cols="80" rows="7"></textarea><br />' +
'<div id="replystatus"></div><button id="replybutton" ' +
'class="btn btn-default btn-sm">' + i18n . _ ( 'Post comment' ) +
'</button></div>'
) ;
reply . find ( 'button' ) . click (
{ parentid : commentid } ,
me . sendComment
) ;
source . after ( reply ) ;
$replyStatus = $ ( '#replystatus' ) ;
$ ( '#replymessage' ) . focus ( ) ;
} ;
2017-02-05 22:09:46 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* send a reply in a discussion
*
* @ name controller . sendComment
* @ function
* @ param { Event } event
* /
me . sendComment = function ( event )
{
event . preventDefault ( ) ;
$errorMessage . addClass ( 'hidden' ) ;
// do not send if no data
var replyMessage = $ ( '#replymessage' ) ;
if ( replyMessage . val ( ) . length === 0 )
2017-02-06 22:39:45 +01:00
{
2017-02-08 20:11:04 +01:00
return ;
}
2017-02-06 22:39:45 +01:00
2017-02-08 20:11:04 +01:00
me . showStatus ( i18n . _ ( 'Sending comment...' ) , true ) ;
var parentid = event . data . parentid ,
key = helper . pageKey ( ) ,
cipherdata = filter . cipher ( key , $passwordInput . val ( ) , replyMessage . val ( ) ) ,
ciphernickname = '' ,
nick = $ ( '#nickname' ) . val ( ) ;
if ( nick . length > 0 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
ciphernickname = filter . cipher ( key , $passwordInput . val ( ) , nick ) ;
}
var data _to _send = {
data : cipherdata ,
parentid : parentid ,
pasteid : helper . pasteId ( ) ,
nickname : ciphernickname
} ;
2012-04-21 21:59:45 +02:00
2017-02-08 20:11:04 +01:00
$ . ajax ( {
type : 'POST' ,
url : helper . scriptLocation ( ) ,
data : data _to _send ,
dataType : 'json' ,
headers : headers ,
success : function ( data )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
if ( data . status === 0 )
{
controller . showStatus ( i18n . _ ( 'Comment posted.' ) ) ;
$ . ajax ( {
type : 'GET' ,
url : helper . scriptLocation ( ) + '?' + helper . pasteId ( ) ,
dataType : 'json' ,
headers : headers ,
success : function ( data )
{
if ( data . status === 0 )
{
controller . displayMessages ( data ) ;
}
else if ( data . status === 1 )
{
controller . showError ( i18n . _ ( 'Could not refresh display: %s' , data . message ) ) ;
}
else
{
controller . showError ( i18n . _ ( 'Could not refresh display: %s' , i18n . _ ( 'unknown status' ) ) ) ;
}
}
} )
. fail ( function ( ) {
controller . showError ( i18n . _ ( 'Could not refresh display: %s' , i18n . _ ( 'server error or not responding' ) ) ) ;
} ) ;
}
else if ( data . status === 1 )
2016-07-11 11:09:41 +02:00
{
2017-02-08 20:11:04 +01:00
controller . showError ( i18n . _ ( 'Could not post comment: %s' , data . message ) ) ;
2016-07-11 11:09:41 +02:00
}
else
{
2017-02-08 20:11:04 +01:00
controller . showError ( i18n . _ ( 'Could not post comment: %s' , i18n . _ ( 'unknown status' ) ) ) ;
2016-07-11 11:09:41 +02:00
}
2017-02-08 13:20:51 +01:00
}
2017-02-08 20:11:04 +01:00
} )
. fail ( function ( ) {
controller . showError ( i18n . _ ( 'Could not post comment: %s' , i18n . _ ( 'server error or not responding' ) ) ) ;
} ) ;
} ;
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* send a new paste to server
*
* @ name controller . sendData
* @ function
* @ param { Event } event
* /
me . sendData = function ( event )
{
event . preventDefault ( ) ;
var file = document . getElementById ( 'file' ) ,
files = ( file && file . files ) ? file . files : null ; // FileList object
// do not send if no data.
if ( $message . val ( ) . length === 0 && ! ( files && files [ 0 ] ) )
{
return ;
}
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
// if sjcl has not collected enough entropy yet, display a message
if ( ! sjcl . random . isReady ( ) )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
me . showStatus ( i18n . _ ( 'Sending paste (Please move your mouse for more entropy)...' ) , true ) ;
sjcl . random . addEventListener ( 'seeded' , function ( ) {
me . sendData ( event ) ;
} ) ;
return ;
}
$ ( '.navbar-toggle' ) . click ( ) ;
$password . addClass ( 'hidden' ) ;
me . showStatus ( i18n . _ ( 'Sending paste...' ) , true ) ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
me . stateSubmittingPaste ( ) ;
var randomkey = sjcl . codec . base64 . fromBits ( sjcl . random . randomWords ( 8 , 0 ) , 0 ) ,
password = $passwordInput . val ( ) ;
if ( files && files [ 0 ] )
2016-08-11 11:31:34 +02:00
{
2017-02-08 20:11:04 +01:00
if ( typeof FileReader === undefined )
2016-08-11 11:31:34 +02:00
{
2017-02-08 20:11:04 +01:00
// revert loading status…
me . stateNewPaste ( ) ;
me . showError ( i18n . _ ( 'Your browser does not support uploading encrypted files. Please use a newer browser.' ) ) ;
return ;
2016-08-11 11:31:34 +02:00
}
2017-02-08 20:11:04 +01:00
var reader = new FileReader ( ) ;
// closure to capture the file information
reader . onload = ( function ( theFile )
2016-08-11 11:31:34 +02:00
{
2017-02-08 20:11:04 +01:00
return function ( e ) {
controller . sendDataContinue (
randomkey ,
filter . cipher ( randomkey , password , e . target . result ) ,
filter . cipher ( randomkey , password , theFile . name )
) ;
} ;
} ) ( files [ 0 ] ) ;
reader . readAsDataURL ( files [ 0 ] ) ;
}
else if ( $attachmentLink . attr ( 'href' ) )
2016-08-18 15:09:58 +02:00
{
2017-02-08 20:11:04 +01:00
me . sendDataContinue (
randomkey ,
filter . cipher ( randomkey , password , $attachmentLink . attr ( 'href' ) ) ,
$attachmentLink . attr ( 'download' )
) ;
}
else
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
me . sendDataContinue ( randomkey , '' , '' ) ;
}
} ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* send a new paste to server , step 2
*
* @ name controller . sendDataContinue
* @ function
* @ param { string } randomkey
* @ param { string } cipherdata _attachment
* @ param { string } cipherdata _attachment _name
* /
me . sendDataContinue = function ( randomkey , cipherdata _attachment , cipherdata _attachment _name )
{
var cipherdata = filter . cipher ( randomkey , $passwordInput . val ( ) , $message . val ( ) ) ,
data _to _send = {
data : cipherdata ,
expire : $ ( '#pasteExpiration' ) . val ( ) ,
formatter : $ ( '#pasteFormatter' ) . val ( ) ,
burnafterreading : $burnAfterReading . is ( ':checked' ) ? 1 : 0 ,
opendiscussion : $openDiscussion . is ( ':checked' ) ? 1 : 0
} ;
if ( cipherdata _attachment . length > 0 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
data _to _send . attachment = cipherdata _attachment ;
if ( cipherdata _attachment _name . length > 0 )
{
data _to _send . attachmentname = cipherdata _attachment _name ;
}
}
$ . ajax ( {
type : 'POST' ,
url : helper . scriptLocation ( ) ,
data : data _to _send ,
dataType : 'json' ,
headers : headers ,
success : function ( data )
{
if ( data . status === 0 ) {
me . stateExistingPaste ( ) ;
var url = helper . scriptLocation ( ) + '?' + data . id + '#' + randomkey ,
deleteUrl = helper . scriptLocation ( ) + '?pasteid=' + data . id + '&deletetoken=' + data . deletetoken ;
me . showStatus ( '' ) ;
$errorMessage . addClass ( 'hidden' ) ;
// show new URL in browser bar
history . pushState ( { type : 'newpaste' } , document . title , url ) ;
$ ( '#pastelink' ) . html (
i18n . _ (
'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>' ,
url , url
) + me . shortenUrl ( url )
) ;
// save newly created element
$pasteUrl = $ ( '#pasteurl' ) ;
// and add click event
$pasteUrl . click ( me . pasteLinkClick ) ;
var shortenButton = $ ( '#shortenbutton' ) ;
if ( shortenButton ) {
shortenButton . click ( me . sendToShortener ) ;
}
$ ( '#deletelink' ) . html ( '<a href="' + deleteUrl + '">' + i18n . _ ( 'Delete data' ) + '</a>' ) ;
$pasteResult . removeClass ( 'hidden' ) ;
// we pre-select the link so that the user only has to [Ctrl]+[c] the link
helper . selectText ( $pasteUrl [ 0 ] ) ;
me . showStatus ( '' ) ;
me . formatPaste ( data _to _send . formatter , $message . val ( ) ) ;
}
else if ( data . status === 1 )
{
// revert loading status…
controller . stateNewPaste ( ) ;
controller . showError ( i18n . _ ( 'Could not create paste: %s' , data . message ) ) ;
}
else
{
// revert loading status…
controller . stateNewPaste ( ) ;
controller . showError ( i18n . _ ( 'Could not create paste: %s' , i18n . _ ( 'unknown status' ) ) ) ;
}
}
} )
. fail ( function ( )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
// revert loading status…
2017-02-08 13:20:51 +01:00
me . stateNewPaste ( ) ;
2017-02-08 20:11:04 +01:00
controller . showError ( i18n . _ ( 'Could not create paste: %s' , i18n . _ ( 'server error or not responding' ) ) ) ;
} ) ;
} ;
2012-04-21 21:59:45 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* check if a URL shortener was defined and create HTML containing a link to it
*
* @ name controller . shortenUrl
* @ function
* @ param { string } url
* @ return { string } html
* /
me . shortenUrl = function ( url )
{
var shortenerHtml = $ ( '#shortenbutton' ) ;
if ( shortenerHtml ) {
shortenerUrl = shortenerHtml . data ( 'shortener' ) ;
createdPasteUrl = url ;
return ' ' + $ ( '<div />' ) . append ( shortenerHtml . clone ( ) ) . html ( ) ;
}
return '' ;
} ;
2012-04-21 21:59:45 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* put the screen in "New paste" mode
*
* @ name controller . stateNewPaste
* @ function
* /
me . stateNewPaste = function ( )
{
$message . text ( '' ) ;
$attachment . addClass ( 'hidden' ) ;
$cloneButton . addClass ( 'hidden' ) ;
$rawTextButton . addClass ( 'hidden' ) ;
$remainingTime . addClass ( 'hidden' ) ;
$pasteResult . addClass ( 'hidden' ) ;
$clearText . addClass ( 'hidden' ) ;
$discussion . addClass ( 'hidden' ) ;
$prettyMessage . addClass ( 'hidden' ) ;
$loadingIndicator . addClass ( 'hidden' ) ;
$sendButton . removeClass ( 'hidden' ) ;
$expiration . removeClass ( 'hidden' ) ;
$formatter . removeClass ( 'hidden' ) ;
$burnAfterReadingOption . removeClass ( 'hidden' ) ;
$openDisc . removeClass ( 'hidden' ) ;
$newButton . removeClass ( 'hidden' ) ;
$password . removeClass ( 'hidden' ) ;
$attach . removeClass ( 'hidden' ) ;
$message . removeClass ( 'hidden' ) ;
$preview . removeClass ( 'hidden' ) ;
$message . focus ( ) ;
} ;
2016-08-09 14:46:32 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* put the screen in mode after submitting a paste
*
* @ name controller . stateSubmittingPaste
* @ function
* /
me . stateSubmittingPaste = function ( )
{
$message . text ( '' ) ;
$attachment . addClass ( 'hidden' ) ;
$cloneButton . addClass ( 'hidden' ) ;
$rawTextButton . addClass ( 'hidden' ) ;
$remainingTime . addClass ( 'hidden' ) ;
$pasteResult . addClass ( 'hidden' ) ;
$clearText . addClass ( 'hidden' ) ;
$discussion . addClass ( 'hidden' ) ;
$prettyMessage . addClass ( 'hidden' ) ;
$sendButton . addClass ( 'hidden' ) ;
$expiration . addClass ( 'hidden' ) ;
$formatter . addClass ( 'hidden' ) ;
$burnAfterReadingOption . addClass ( 'hidden' ) ;
$openDisc . addClass ( 'hidden' ) ;
$newButton . addClass ( 'hidden' ) ;
$password . addClass ( 'hidden' ) ;
$attach . addClass ( 'hidden' ) ;
$message . addClass ( 'hidden' ) ;
$preview . addClass ( 'hidden' ) ;
$loadingIndicator . removeClass ( 'hidden' ) ;
} ;
2016-08-11 11:40:37 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* put the screen in a state where the only option is to submit a
* new paste
*
* @ name controller . stateOnlyNewPaste
* @ function
* /
me . stateOnlyNewPaste = function ( )
{
$message . text ( '' ) ;
$attachment . addClass ( 'hidden' ) ;
$cloneButton . addClass ( 'hidden' ) ;
$rawTextButton . addClass ( 'hidden' ) ;
$remainingTime . addClass ( 'hidden' ) ;
$pasteResult . addClass ( 'hidden' ) ;
$clearText . addClass ( 'hidden' ) ;
$discussion . addClass ( 'hidden' ) ;
$prettyMessage . addClass ( 'hidden' ) ;
$sendButton . addClass ( 'hidden' ) ;
$expiration . addClass ( 'hidden' ) ;
$formatter . addClass ( 'hidden' ) ;
$burnAfterReadingOption . addClass ( 'hidden' ) ;
$openDisc . addClass ( 'hidden' ) ;
$password . addClass ( 'hidden' ) ;
$attach . addClass ( 'hidden' ) ;
$message . addClass ( 'hidden' ) ;
$preview . addClass ( 'hidden' ) ;
$loadingIndicator . addClass ( 'hidden' ) ;
$newButton . removeClass ( 'hidden' ) ;
} ;
2016-08-09 14:46:32 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* put the screen in "Existing paste" mode
*
* @ name controller . stateExistingPaste
* @ function
* @ param { boolean } [ preview = false ] - ( optional ) tell if the preview tabs should be displayed , defaults to false
* /
me . stateExistingPaste = function ( preview )
{
preview = preview || false ;
2016-08-09 14:46:32 +02:00
2017-02-08 20:11:04 +01:00
if ( ! preview )
2015-10-15 22:06:01 +02:00
{
2017-02-08 20:11:04 +01:00
// no "clone" for IE<10.
if ( $ ( '#oldienotice' ) . is ( ":visible" ) )
2015-10-15 22:06:01 +02:00
{
2017-02-08 20:11:04 +01:00
$cloneButton . addClass ( 'hidden' ) ;
}
else
{
$cloneButton . removeClass ( 'hidden' ) ;
2015-10-15 22:06:01 +02:00
}
2017-02-08 20:11:04 +01:00
$rawTextButton . removeClass ( 'hidden' ) ;
$sendButton . addClass ( 'hidden' ) ;
$attach . addClass ( 'hidden' ) ;
$expiration . addClass ( 'hidden' ) ;
$formatter . addClass ( 'hidden' ) ;
$burnAfterReadingOption . addClass ( 'hidden' ) ;
$openDisc . addClass ( 'hidden' ) ;
$newButton . removeClass ( 'hidden' ) ;
$preview . addClass ( 'hidden' ) ;
}
2017-02-08 13:20:51 +01:00
2017-02-08 20:11:04 +01:00
$pasteResult . addClass ( 'hidden' ) ;
$message . addClass ( 'hidden' ) ;
$clearText . addClass ( 'hidden' ) ;
$prettyMessage . addClass ( 'hidden' ) ;
$loadingIndicator . addClass ( 'hidden' ) ;
} ;
2016-07-11 11:09:41 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* when "burn after reading" is checked , disable discussion
*
* @ name controller . changeBurnAfterReading
* @ function
* /
me . changeBurnAfterReading = function ( )
{
if ( $burnAfterReading . is ( ':checked' ) )
2016-07-11 11:09:41 +02:00
{
2017-02-08 20:11:04 +01:00
$openDisc . addClass ( 'buttondisabled' ) ;
$openDiscussion . attr ( { checked : false , disabled : true } ) ;
}
else
2017-02-05 14:47:03 +01:00
{
2017-02-08 20:11:04 +01:00
$openDisc . removeClass ( 'buttondisabled' ) ;
$openDiscussion . removeAttr ( 'disabled' ) ;
}
} ;
2017-02-05 14:47:03 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* when discussion is checked , disable "burn after reading"
*
* @ name controller . changeOpenDisc
* @ function
* /
me . changeOpenDisc = function ( )
{
if ( $openDiscussion . is ( ':checked' ) )
2017-02-05 21:22:09 +01:00
{
2017-02-08 20:11:04 +01:00
$burnAfterReadingOption . addClass ( 'buttondisabled' ) ;
$burnAfterReading . attr ( { checked : false , disabled : true } ) ;
}
else
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
$burnAfterReadingOption . removeClass ( 'buttondisabled' ) ;
$burnAfterReading . removeAttr ( 'disabled' ) ;
}
} ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* forward to URL shortener
*
* @ name controller . sendToShortener
* @ function
* @ param { Event } event
* /
me . sendToShortener = function ( event )
{
window . location . href = shortenerUrl + encodeURIComponent ( createdPasteUrl ) ;
event . preventDefault ( ) ;
} ;
2015-09-16 22:51:48 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* reload the page
*
* This takes the user to the PrivateBin home page .
*
* @ name controller . reloadPage
* @ function
* @ param { Event } event
* /
me . reloadPage = function ( event )
{
window . location . href = helper . scriptLocation ( ) ;
event . preventDefault ( ) ;
} ;
/ * *
* return raw text
*
* @ name controller . rawText
* @ function
* @ param { Event } event
* /
me . rawText = function ( event )
{
var paste = $ ( '#pasteFormatter' ) . val ( ) === 'markdown' ?
$prettyPrint . text ( ) : $clearText . text ( ) ;
history . pushState (
null , document . title , helper . scriptLocation ( ) + '?' +
helper . pasteId ( ) + '#' + helper . pageKey ( )
) ;
// we use text/html instead of text/plain to avoid a bug when
// reloading the raw text view (it reverts to type text/html)
var newDoc = document . open ( 'text/html' , 'replace' ) ;
newDoc . write ( '<pre>' + helper . htmlEntities ( paste ) + '</pre>' ) ;
newDoc . close ( ) ;
event . preventDefault ( ) ;
} ;
/ * *
* clone the current paste
*
* @ name controller . clonePaste
* @ function
* @ param { Event } event
* /
me . clonePaste = function ( event )
{
event . preventDefault ( ) ;
me . stateNewPaste ( ) ;
// erase the id and the key in url
history . replaceState ( null , document . title , helper . scriptLocation ( ) ) ;
me . showStatus ( '' ) ;
if ( $attachmentLink . attr ( 'href' ) )
2016-11-13 18:12:10 +01:00
{
2017-02-08 20:11:04 +01:00
$clonedFile . removeClass ( 'hidden' ) ;
$fileWrap . addClass ( 'hidden' ) ;
}
$message . text (
$ ( '#pasteFormatter' ) . val ( ) === 'markdown' ?
$prettyPrint . text ( ) : $clearText . text ( )
) ;
$ ( '.navbar-toggle' ) . click ( ) ;
} ;
2016-11-13 18:12:10 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* set the expiration on bootstrap templates
*
* @ name controller . setExpiration
* @ function
* @ param { Event } event
* /
me . setExpiration = function ( event )
{
event . preventDefault ( ) ;
var target = $ ( event . target ) ;
$ ( '#pasteExpiration' ) . val ( target . data ( 'expiration' ) ) ;
$ ( '#pasteExpirationDisplay' ) . text ( target . text ( ) ) ;
} ;
/ * *
* set the format on bootstrap templates
*
* @ name controller . setFormat
* @ function
* @ param { Event } event
* /
me . setFormat = function ( event )
{
var target = $ ( event . target ) ;
$ ( '#pasteFormatter' ) . val ( target . data ( 'format' ) ) ;
$ ( '#pasteFormatterDisplay' ) . text ( target . text ( ) ) ;
if ( $messagePreview . parent ( ) . hasClass ( 'active' ) ) {
me . viewPreview ( event ) ;
}
event . preventDefault ( ) ;
} ;
/ * *
* set the language in a cookie and reload the page
*
* @ name controller . setLanguage
* @ function
* @ param { Event } event
* /
me . setLanguage = function ( event )
{
document . cookie = 'lang=' + $ ( event . target ) . data ( 'lang' ) ;
me . reloadPage ( event ) ;
} ;
/ * *
* support input of tab character
*
* @ name controller . supportTabs
* @ function
* @ param { Event } event
* @ TODO doc what is @ this here ?
* /
me . supportTabs = function ( event )
{
var keyCode = event . keyCode || event . which ;
// tab was pressed
if ( keyCode === 9 )
2016-11-13 18:12:10 +01:00
{
2017-02-08 20:11:04 +01:00
// prevent the textarea to lose focus
2016-11-13 18:12:10 +01:00
event . preventDefault ( ) ;
2017-02-08 20:11:04 +01:00
// get caret position & selection
var 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 ;
}
} ;
/ * *
* view the editor tab
*
* @ name controller . viewEditor
* @ function
* @ param { Event } event
* /
me . viewEditor = function ( event )
{
$messagePreview . parent ( ) . removeClass ( 'active' ) ;
$messageEdit . parent ( ) . addClass ( 'active' ) ;
$message . focus ( ) ;
me . stateNewPaste ( ) ;
event . preventDefault ( ) ;
} ;
/ * *
* view the preview tab
*
* @ name controller . viewPreview
* @ function
* @ param { Event } event
* /
me . viewPreview = function ( event )
{
$messageEdit . parent ( ) . removeClass ( 'active' ) ;
$messagePreview . parent ( ) . addClass ( 'active' ) ;
$message . focus ( ) ;
me . stateExistingPaste ( true ) ;
me . formatPaste ( $ ( '#pasteFormatter' ) . val ( ) , $message . val ( ) ) ;
event . preventDefault ( ) ;
} ;
2016-11-13 18:12:10 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* handle history ( pop ) state changes
*
* currently this does only handle redirects to the home page .
*
* @ name controller . historyChange
* @ function
* @ param { Event } event
* /
me . historyChange = function ( event )
{
var currentLocation = helper . scriptLocation ( ) ;
if ( event . originalEvent . state === null && // no state object passed
event . originalEvent . 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 ;
}
} ;
/ * *
* 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 controller . pasteLinkClick
* @ function
* @ param { Event } event
* /
me . pasteLinkClick = function ( event )
{
// 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 ) ;
}
} ;
/ * *
* create a new paste
*
* @ name controller . newPaste
* @ function
* /
me . newPaste = function ( )
{
me . stateNewPaste ( ) ;
me . showStatus ( '' ) ;
$message . text ( '' ) ;
me . changeBurnAfterReading ( ) ;
me . changeOpenDisc ( ) ;
} ;
/ * *
* removes an attachment
*
* @ name controller . removeAttachment
* @ function
* /
me . removeAttachment = function ( )
{
$clonedFile . addClass ( 'hidden' ) ;
// removes the saved decrypted file data
$attachmentLink . attr ( 'href' , '' ) ;
// the only way to deselect the file is to recreate the input // @TODO really?
$fileWrap . html ( $fileWrap . html ( ) ) ;
$fileWrap . removeClass ( 'hidden' ) ;
} ;
/ * *
* decrypt using the password from the modal dialog
*
* @ name controller . decryptPasswordModal
* @ function
* /
me . decryptPasswordModal = function ( )
{
$passwordInput . val ( $passwordDecrypt . val ( ) ) ;
me . displayMessages ( ) ;
} ;
/ * *
* submit a password in the modal dialog
*
* @ name controller . submitPasswordModal
* @ function
* @ param { Event } event
* /
me . submitPasswordModal = function ( event )
{
event . preventDefault ( ) ;
$passwordModal . modal ( 'hide' ) ;
} ;
/ * *
* display an error message ,
* we use the same function for paste and reply to comments
*
* @ name controller . showError
* @ function
* @ param { string } message - text to display
* /
me . showError = function ( message )
{
if ( $status . length )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
$status . addClass ( 'errorMessage' ) . text ( message ) ;
}
else
{
$errorMessage . removeClass ( 'hidden' ) ;
helper . setMessage ( $errorMessage , message ) ;
}
if ( typeof $replyStatus !== 'undefined' ) {
$replyStatus . addClass ( 'errorMessage' ) ;
$replyStatus . addClass ( $errorMessage . attr ( 'class' ) ) ;
2017-02-08 13:20:51 +01:00
if ( $status . length )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
$replyStatus . html ( $status . html ( ) ) ;
2015-09-05 17:12:11 +02:00
}
else
{
2017-02-08 20:11:04 +01:00
$replyStatus . html ( $errorMessage . html ( ) ) ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
}
} ;
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* display a status message ,
* we use the same function for paste and reply to comments
*
* @ name controller . showStatus
* @ function
* @ param { string } message - text to display
* @ param { boolean } [ spin = false ] - ( optional ) tell if the "spinning" animation should be displayed , defaults to false
* /
me . showStatus = function ( message , spin )
{
if ( spin || false )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0 4px 0 0;" />' ;
$status . prepend ( img ) ;
2017-02-08 13:20:51 +01:00
if ( typeof $replyStatus !== 'undefined' ) {
2017-02-08 20:11:04 +01:00
$replyStatus . prepend ( img ) ;
2015-09-05 17:12:11 +02:00
}
2017-02-08 20:11:04 +01:00
}
if ( typeof $replyStatus !== 'undefined' ) {
$replyStatus . removeClass ( 'errorMessage' ) . text ( message ) ;
}
if ( ! message )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
$status . html ( ' ' ) ;
return ;
}
if ( message === '' )
{
$status . html ( ' ' ) ;
return ;
}
$status . removeClass ( 'errorMessage' ) . text ( message ) ;
} ;
2017-02-01 19:24:56 +01:00
2017-02-08 20:11:04 +01:00
/ * *
* bind events to DOM elements
*
* @ private
* @ function
* /
function bindEvents ( )
{
$burnAfterReading . change ( me . changeBurnAfterReading ) ;
$openDisc . change ( me . changeOpenDisc ) ;
$sendButton . click ( me . sendData ) ;
$cloneButton . click ( me . clonePaste ) ;
$rawTextButton . click ( me . rawText ) ;
$fileRemoveButton . click ( me . removeAttachment ) ;
$ ( '.reloadlink' ) . click ( me . reloadPage ) ;
$message . keydown ( me . supportTabs ) ;
$messageEdit . click ( me . viewEditor ) ;
$messagePreview . click ( me . viewPreview ) ;
// bootstrap template drop downs
$ ( 'ul.dropdown-menu li a' , $ ( '#expiration' ) . parent ( ) ) . click ( me . setExpiration ) ;
$ ( 'ul.dropdown-menu li a' , $ ( '#formatter' ) . parent ( ) ) . click ( me . setFormat ) ;
$ ( '#language ul.dropdown-menu li a' ) . click ( me . setLanguage ) ;
// page template drop down
$ ( '#language select option' ) . click ( me . setLanguage ) ;
// focus password input when it is shown
$passwordModal . on ( 'shown.bs.modal' , function ( ) {
$passwordDecrypt . focus ( ) ;
} ) ;
// handle modal password request on decryption
$passwordModal . on ( 'hidden.bs.modal' , me . decryptPasswordModal ) ;
$passwordForm . submit ( me . submitPasswordModal ) ;
$ ( window ) . on ( 'popstate' , me . historyChange ) ;
}
2015-09-05 17:12:11 +02:00
2017-02-08 20:11:04 +01:00
/ * *
* main application
*
* @ name controller . init
* @ function
* /
me . init = function ( )
{
// hide "no javascript" message
$ ( '#noscript' ) . hide ( ) ;
// preload jQuery wrapped DOM elements and bind events
$attach = $ ( '#attach' ) ;
$attachment = $ ( '#attachment' ) ;
$attachmentLink = $ ( '#attachment a' ) ;
$burnAfterReading = $ ( '#burnafterreading' ) ;
$burnAfterReadingOption = $ ( '#burnafterreadingoption' ) ;
$cipherData = $ ( '#cipherdata' ) ;
$clearText = $ ( '#cleartext' ) ;
$cloneButton = $ ( '#clonebutton' ) ;
$clonedFile = $ ( '#clonedfile' ) ;
$comments = $ ( '#comments' ) ;
$discussion = $ ( '#discussion' ) ;
$errorMessage = $ ( '#errormessage' ) ;
$expiration = $ ( '#expiration' ) ;
$fileRemoveButton = $ ( '#fileremovebutton' ) ;
$fileWrap = $ ( '#filewrap' ) ;
$formatter = $ ( '#formatter' ) ;
$image = $ ( '#image' ) ;
$loadingIndicator = $ ( '#loadingindicator' ) ;
$message = $ ( '#message' ) ;
$messageEdit = $ ( '#messageedit' ) ;
$messagePreview = $ ( '#messagepreview' ) ;
$newButton = $ ( '#newbutton' ) ;
$openDisc = $ ( '#opendisc' ) ;
$openDiscussion = $ ( '#opendiscussion' ) ;
$password = $ ( '#password' ) ;
$passwordInput = $ ( '#passwordinput' ) ;
$passwordModal = $ ( '#passwordmodal' ) ;
$passwordForm = $ ( '#passwordform' ) ;
$passwordDecrypt = $ ( '#passworddecrypt' ) ;
$pasteResult = $ ( '#pasteresult' ) ;
// $pasteUrl is saved in sendDataContinue() if/after it is
// actually created
$prettyMessage = $ ( '#prettymessage' ) ;
$prettyPrint = $ ( '#prettyprint' ) ;
$preview = $ ( '#preview' ) ;
$rawTextButton = $ ( '#rawtextbutton' ) ;
$remainingTime = $ ( '#remainingtime' ) ;
// $replyStatus is saved in openReply()
$sendButton = $ ( '#sendbutton' ) ;
$status = $ ( '#status' ) ;
bindEvents ( ) ;
// display status returned by php code, if any (eg. paste was properly deleted)
if ( $status . text ( ) . length > 0 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
me . showStatus ( $status . text ( ) ) ;
return ;
}
2012-04-23 16:30:02 +02:00
2017-02-08 20:11:04 +01:00
// keep line height even if content empty
$status . html ( ' ' ) ;
2012-04-23 16:30:02 +02:00
2017-02-08 20:11:04 +01:00
// display an existing paste
if ( $cipherData . text ( ) . length > 1 )
{
// missing decryption key in URL?
if ( window . location . hash . length === 0 )
2015-09-05 17:12:11 +02:00
{
2017-02-08 20:11:04 +01:00
me . showError ( i18n . _ ( 'Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)' ) ) ;
return ;
2015-09-05 17:12:11 +02:00
}
2013-11-01 01:15:14 +01:00
2017-02-08 20:11:04 +01:00
// show proper elements on screen
me . stateExistingPaste ( ) ;
me . displayMessages ( ) ;
}
// display error message from php code
else if ( $errorMessage . text ( ) . length > 1 )
{
me . showError ( $errorMessage . text ( ) ) ;
}
// create a new paste
else
{
me . newPaste ( ) ;
}
2017-01-22 10:42:11 +01:00
} ;
2017-02-08 20:11:04 +01:00
return me ;
} ) ( window , document , jQuery , sjcl , Base64 , RawDeflate ) ;