var Decrees = module . exports ;
var Core = require ( "./commands/core" ) ;
/ * A d m i n d e c r e e s w h i c h m o d i f y g l o b a l s e r v e r s t a t e
IMPLEMENTED :
RESTRICT _REGISTRATION ( < boolean > )
UPDATE _DEFAULT _STORAGE ( < number > )
// QUOTA MANAGEMENT
SET _QUOTA ( < string : signkey > , limit )
RM _QUOTA ( < string : signkey > )
// INACTIVITY
SET _INACTIVE _TIME
SET _ACCOUNT _RETENTION _TIME
SET _ARCHIVE _RETENTION _TIME
// UPLOADS
SET _MAX _UPLOAD _SIZE
SET _PREMIUM _UPLOAD _SIZE
// BACKGROUND PROCESSES
DISABLE _INTEGRATED _TASKS
DISABLE _INTEGRATED _EVICTION
ENABLE _PROFILING
SET _PROFILING _WINDOW
// BROADCAST
SET _LAST _BROADCAST _HASH
SET _SURVEY _URL
SET _MAINTENANCE
// EASIER CONFIG
SET _ADMIN _EMAIL
SET _SUPPORT _MAILBOX
// COMMUNITY PARTICIPATION AND GOVERNANCE
CONSENT _TO _CONTACT
LIST _MY _INSTANCE
PROVIDE _AGGREGATE _STATISTICS
REMOVE _DONATE _BUTTON
BLOCK _DAILY _CHECK
// Customized instance info
SET _INSTANCE _JURISDICTION
SET _INSTANCE _DESCRIPTION
SET _INSTANCE _NAME
SET _INSTANCE _NOTICE
NOT IMPLEMENTED :
// RESTRICTED REGISTRATION
ADD _INVITE
REVOKE _INVITE
REDEEM _INVITE
DISABLE _EMBEDDING
// 2.0
Env . DEV _MODE || Env . FRESH _MODE ,
ADD _ADMIN _KEY
RM _ADMIN _KEY
* /
var commands = { } ;
/ * c o m m a n d s h a v e a s i m p l e A P I :
* they receive the global Env and the arguments to be applied
* if the arguments are invalid the operation will not be applied
* the command throws
* if the arguments are valid but do not result in a change , the operation is redundant .
* return false
* if the arguments are valid and will result in a change , the operation should be applied
* apply it
* return true to indicate that it was applied
* /
var args _isBoolean = function ( args ) {
return ! ( ! Array . isArray ( args ) || typeof ( args [ 0 ] ) !== 'boolean' ) ;
} ;
// Toggles a simple boolean
var makeBooleanSetter = function ( attr ) {
return function ( Env , args ) {
if ( ! args _isBoolean ( args ) ) {
throw new Error ( 'INVALID_ARGS' ) ;
}
var bool = args [ 0 ] ;
if ( bool === Env [ attr ] ) { return false ; }
Env [ attr ] = bool ;
return true ;
} ;
} ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
commands . ENABLE _EMBEDDING = makeBooleanSetter ( 'enableEmbedding' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
commands . RESTRICT _REGISTRATION = makeBooleanSetter ( 'restrictRegistration' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_EVICTION', [true]]], console.log)
commands . DISABLE _INTEGRATED _EVICTION = makeBooleanSetter ( 'disableIntegratedEviction' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_TASKS', [true]]], console.log)
commands . DISABLE _INTEGRATED _TASKS = makeBooleanSetter ( 'disableIntegratedTasks' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['CONSENT_TO_CONTACT', [true]]], console.log)
commands . CONSENT _TO _CONTACT = makeBooleanSetter ( 'consentToContact' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['LIST_MY_INSTANCE', [true]]], console.log)
commands . LIST _MY _INSTANCE = makeBooleanSetter ( 'listMyInstance' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['PROVIDE_AGGREGATE_STATISTICS', [true]]], console.log)
commands . PROVIDE _AGGREGATE _STATISTICS = makeBooleanSetter ( 'provideAggregateStatistics' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['REMOVE_DONATE_BUTTON', [true]]], console.log)
commands . REMOVE _DONATE _BUTTON = makeBooleanSetter ( 'removeDonateButton' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['BLOCK_DAILY_CHECK', [true]]], console.log)
commands . BLOCK _DAILY _CHECK = makeBooleanSetter ( 'blockDailyCheck' ) ;
/ *
var isNonNegativeNumber = function ( n ) {
return ! ( typeof ( n ) !== 'number' || isNaN ( n ) || n < 0 ) ;
} ;
* /
var default _validator = function ( ) { return true ; } ;
var makeGenericSetter = function ( attr , validator ) {
validator = validator || default _validator ;
return function ( Env , args ) {
if ( ! validator ( args ) ) {
throw new Error ( "INVALID_ARGS" ) ;
}
var value = args [ 0 ] ;
if ( value === Env [ attr ] ) { return false ; }
Env [ attr ] = value ;
return true ;
} ;
} ;
var isInteger = function ( n ) {
return ! ( typeof ( n ) !== 'number' || isNaN ( n ) || ( n % 1 ) !== 0 ) ;
} ;
var args _isInteger = function ( args ) {
return ! ( ! Array . isArray ( args ) || ! isInteger ( args [ 0 ] ) ) ;
} ;
var makeIntegerSetter = function ( attr ) {
return makeGenericSetter ( attr , args _isInteger ) ;
} ;
var arg _isPositiveInteger = function ( args ) {
return Array . isArray ( args ) && isInteger ( args [ 0 ] ) && args [ 0 ] > 0 ;
} ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENABLE_PROFILING', [true]]], console.log)
commands . ENABLE _PROFILING = makeBooleanSetter ( 'enableProfiling' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_PROFILING_WINDOW', [10000]]], console.log)
commands . SET _PROFILING _WINDOW = makeGenericSetter ( 'profilingWindow' , arg _isPositiveInteger ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAX_UPLOAD_SIZE', [50 * 1024 * 1024]]], console.log)
commands . SET _MAX _UPLOAD _SIZE = makeIntegerSetter ( 'maxUploadSize' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_PREMIUM_UPLOAD_SIZE', [150 * 1024 * 1024]]], console.log)
commands . SET _PREMIUM _UPLOAD _SIZE = makeIntegerSetter ( 'premiumUploadSize' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['UPDATE_DEFAULT_STORAGE', [100 * 1024 * 1024]]], console.log)
commands . UPDATE _DEFAULT _STORAGE = makeIntegerSetter ( 'defaultStorageLimit' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INACTIVE_TIME', [90]]], console.log)
commands . SET _INACTIVE _TIME = makeIntegerSetter ( 'inactiveTime' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ARCHIVE_RETENTION_TIME', [30]]], console.log)
commands . SET _ARCHIVE _RETENTION _TIME = makeIntegerSetter ( 'archiveRetentionTime' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ACCOUNT_RETENTION_TIME', [365]]], console.log)
commands . SET _ACCOUNT _RETENTION _TIME = makeIntegerSetter ( 'accountRetentionTime' ) ;
var args _isString = function ( args ) {
return Array . isArray ( args ) && typeof ( args [ 0 ] ) === "string" ;
} ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ADMIN_EMAIL', ['admin@website.tld']]], console.log)
commands . SET _ADMIN _EMAIL = makeGenericSetter ( 'adminEmail' , args _isString ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SUPPORT_MAILBOX', ["Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA="]]], console.log)
commands . SET _SUPPORT _MAILBOX = makeGenericSetter ( 'supportMailbox' , function ( args ) {
return args _isString ( args ) && Core . isValidPublicKey ( args [ 0 ] ) ;
} ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_PURPOSE', ["development"]]], console.log)
commands . SET _INSTANCE _PURPOSE = makeGenericSetter ( 'instancePurpose' , args _isString ) ;
var makeTranslation = function ( attr ) {
return function ( Env , args ) {
if ( ! Array . isArray ( args ) ) { throw new Error ( "INVALID_ARGS" ) ; }
var value = args [ 0 ] ;
var state = Env [ attr ] ;
if ( typeof ( value ) === 'string' ) {
if ( state . default === value ) { return false ; }
state . default = value ;
return true ;
}
if ( value && typeof ( value ) === 'object' ) {
var changed = false ;
Object . keys ( value ) . forEach ( function ( lang ) {
if ( state [ lang ] === value [ lang ] ) { return ; }
state [ lang ] = value [ lang ] ;
changed = true ;
} ) ;
return changed ;
}
return false ;
} ;
} ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_JURISDICTION', ['France']]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_JURISDICTION', [{default:'France',de:'Frankreich'}]]], console.log)
commands . SET _INSTANCE _JURISDICTION = makeTranslation ( 'instanceJurisdiction' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_NAME', ['My Personal CryptPad']]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_NAME', [{default:'My Personal CryptPad', fr: "Mon CryptPad personnel"}]]], console.log)
commands . SET _INSTANCE _NAME = makeTranslation ( 'instanceName' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_DESCRIPTION', ['A personal instance, hosted for me and nobody else']]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_DESCRIPTION', [{default:'A personal server, not intended for public usage', fr: 'Un serveur personnel, non destiné à un usage public'}]]], console.log)
commands . SET _INSTANCE _DESCRIPTION = makeTranslation ( 'instanceDescription' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_NOTICE', ['Our hosting costs have increased during the pandemic. Please consider donating!']]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_NOTICE', [{default:'Our hosting costs have increased during the pandemic. Please consider donating!',fr:'Nos coûts d'hébergement ont augmenté pendant la pandémie. Veuillez envisager de faire un don !']]], console.log)
commands . SET _INSTANCE _NOTICE = makeTranslation ( 'instanceNotice' ) ;
// Maintenance: Empty string or an object with a start and end time
var isNumber = function ( value ) {
return typeof ( value ) === "number" && ! isNaN ( value ) ;
} ;
var args _isMaintenance = function ( args ) {
return Array . isArray ( args ) && args [ 0 ] &&
( args [ 0 ] === "" || ( isNumber ( args [ 0 ] . end ) && isNumber ( args [ 0 ] . start ) ) ) ;
} ;
// we anticipate that we'll add language-specific surveys in the future
// whenever that happens we can relax validation a bit to support more formats
var makeBroadcastSetter = function ( attr , validation ) {
return function ( Env , args ) {
if ( ( validation && ! validation ( args ) ) && ! args _isString ( args ) ) {
throw new Error ( 'INVALID_ARGS' ) ;
}
var str = args [ 0 ] ;
if ( str === Env [ attr ] ) { return false ; }
Env [ attr ] = str ;
Env . broadcastCache = { } ;
return true ;
} ;
} ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_LAST_BROADCAST_HASH', [hash]]], console.log)
commands . SET _LAST _BROADCAST _HASH = makeBroadcastSetter ( 'lastBroadcastHash' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SURVEY_URL', [url]]], console.log)
commands . SET _SURVEY _URL = makeBroadcastSetter ( 'surveyURL' ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [{start: +Date, end: +Date}]]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [""]]], console.log)
commands . SET _MAINTENANCE = makeBroadcastSetter ( 'maintenance' , args _isMaintenance ) ;
var Quota = require ( "./commands/quota" ) ;
var Keys = require ( "./keys" ) ;
var Util = require ( "./common-util" ) ;
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_QUOTA', ['[user@box:3000/VzeS4vP1DF+tXGuq1i50DKYuBL+09Yqy8kGxoUKRzhA=]', { limit: 2 * 1024 * 1024 * 1024, plan: 'buddy', note: "you're welcome" } ] ] ], console.log)
commands . SET _QUOTA = function ( Env , args ) {
if ( ! Array . isArray ( args ) || args . length !== 2 ) {
throw new Error ( "INVALID_ARGS" ) ;
}
var unsafeKey = Keys . canonicalize ( args [ 0 ] ) ;
if ( ! unsafeKey ) {
throw new Error ( "INVALID_ARGS" ) ;
}
// make sure you're not overwriting an existing limit
//if (Env.customLimits[unsafeKey]) { throw new Error("EEXISTS"); }
var limit = args [ 1 ] ;
if ( ! Quota . isValidLimit ( limit ) ) { // do we really want this?
throw new Error ( "INVALID_ARGS" ) ;
}
limit . origin = 'decree' ;
// map the new limit to the user's unsafeKey
Env . customLimits [ unsafeKey ] = limit ;
Env . limits [ unsafeKey ] = limit ;
return true ;
} ;
commands . RM _QUOTA = function ( Env , args ) {
if ( ! Array . isArray ( args ) || args . length !== 1 ) {
throw new Error ( "INVALID_ARGS" ) ;
}
var unsafeKey = Keys . canonicalize ( args [ 0 ] ) ;
if ( ! unsafeKey ) {
throw new Error ( "INVALID_ARGS" ) ;
}
if ( ! Env . customLimits [ unsafeKey ] ) {
throw new Error ( "ENOENT" ) ;
}
delete Env . customLimits [ unsafeKey ] ;
delete Env . limits [ unsafeKey ] ;
return true ;
} ;
// [<command>, <args>, <author>, <time>]
var handleCommand = Decrees . handleCommand = function ( Env , line ) {
var command = line [ 0 ] ;
var args = line [ 1 ] ;
if ( typeof ( commands [ command ] ) !== 'function' ) {
throw new Error ( "DECREE_UNSUPPORTED_COMMAND" ) ;
}
return commands [ command ] ( Env , args ) ;
} ;
Decrees . createLineHandler = function ( Env ) {
var Log = Env . Log ;
var index = - 1 ;
return function ( err , line ) {
index ++ ;
if ( err ) {
// Log the error and bail out
return void Log . error ( "DECREE_LINE_ERR" , {
error : err . message ,
index : index ,
line : line ,
} ) ;
}
if ( Array . isArray ( line ) ) {
try {
return void handleCommand ( Env , line ) ;
} catch ( err2 ) {
return void Log . error ( "DECREE_COMMAND_ERR" , {
error : err2 . message ,
index : index ,
line : line ,
} ) ;
}
}
Log . error ( "DECREE_HANDLER_WEIRD_LINE" , {
line : line ,
index : index ,
} ) ;
} ;
} ;
var Fs = require ( "fs" ) ;
var Path = require ( "path" ) ;
var readFileBin = require ( "./stream-file" ) . readFileBin ;
var Schedule = require ( "./schedule" ) ;
var Fse = require ( "fs-extra" ) ;
var nThen = require ( "nthen" ) ;
Decrees . load = function ( Env , _cb ) {
Env . scheduleDecree = Env . scheduleDecree || Schedule ( ) ;
var cb = Util . once ( Util . mkAsync ( function ( err ) {
if ( err && err . code !== 'ENOENT' ) {
return void _cb ( err ) ;
}
_cb ( ) ;
} ) ) ;
Env . scheduleDecree . blocking ( '' , function ( unblock ) {
var done = Util . once ( Util . both ( cb , unblock ) ) ;
nThen ( function ( w ) {
// ensure that the path to the decree log exists
Fse . mkdirp ( Env . paths . decree , w ( function ( err ) {
if ( ! err ) { return ; }
w . abort ( ) ;
done ( err ) ;
} ) ) ;
} ) . nThen ( function ( ) {
var decreeName = Path . join ( Env . paths . decree , 'decree.ndjson' ) ;
var stream = Fs . createReadStream ( decreeName , { start : 0 } ) ;
var handler = Decrees . createLineHandler ( Env ) ;
readFileBin ( stream , function ( msgObj , next ) {
var text = msgObj . buff . toString ( 'utf8' ) ;
try {
handler ( void 0 , JSON . parse ( text ) ) ;
} catch ( err ) {
handler ( err , text ) ;
}
next ( ) ;
} , function ( err ) {
done ( err ) ;
} ) ;
} ) ;
} ) ;
} ;
Decrees . write = function ( Env , decree , _cb ) {
var path = Path . join ( Env . paths . decree , 'decree.ndjson' ) ;
Env . scheduleDecree . ordered ( '' , function ( next ) {
var cb = Util . both ( _cb , next ) ;
Fs . appendFile ( path , JSON . stringify ( decree ) + '\n' , cb ) ;
} ) ;
} ;