2016-09-27 09:00:02 -04:00
< ? php
2017-11-16 20:23:18 -05:00
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
2017-07-16 22:27:55 -04:00
/** Conf class */
2016-09-27 09:00:02 -04:00
declare ( strict_types = 1 );
2017-03-28 00:12:12 -04:00
namespace JKingWeb\Arsse ;
2016-09-27 09:00:02 -04:00
2019-01-20 22:40:49 -05:00
use JKingWeb\Arsse\Misc\ValueInfo as Value ;
2017-07-16 22:27:55 -04:00
/** Class for loading , saving , and querying configuration
2017-08-29 10:50:31 -04:00
*
2017-07-27 09:09:39 -04:00
* The Conf class serves both as a means of importing and querying configuration information , as well as a source for default parameters when a configuration file does not specify a value .
* All public properties are configuration parameters that may be set by the server administrator . */
2016-09-27 09:00:02 -04:00
class Conf {
2017-07-16 22:27:55 -04:00
/** @var string Default language to use for logging and errors */
2017-02-16 15:29:42 -05:00
public $lang = " en " ;
2016-10-15 09:45:23 -04:00
2019-01-20 22:40:49 -05:00
/** @var string The database driver to use, one of "sqlite3", "postgresql", or "mysql". A fully-qualified class name may also be used for custom drivers */
public $dbDriver = " sqlite3 " ;
/** @var boolean Whether to attempt to automatically update the database when upgrading to a new version with schema changes */
2017-07-11 20:27:37 -04:00
public $dbAutoUpdate = true ;
2019-01-23 16:31:54 -05:00
/** @var \DateInterval|null Number of seconds to wait before returning a timeout error when connecting to a database (null waits forever; not applicable to SQLite) */
2018-11-22 13:30:13 -05:00
public $dbTimeoutConnect = 5.0 ;
2019-01-23 16:31:54 -05:00
/** @var \DateInterval|null Number of seconds to wait before returning a timeout error when executing a database operation (null waits forever; not applicable to SQLite) */
public $dbTimeoutExec = null ;
/** @var \DateInterval|null Number of seconds to wait before returning a timeout error when acquiring a database lock (null waits forever) */
public $dbTimeoutLock = 60.0 ;
2017-08-28 19:38:58 -04:00
/** @var string|null Full path and file name of SQLite database (if using SQLite) */
public $dbSQLite3File = null ;
2017-07-16 22:27:55 -04:00
/** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */
2017-02-16 15:29:42 -05:00
public $dbSQLite3Key = " " ;
2018-11-10 00:02:38 -05:00
/** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */
public $dbPostgreSQLHost = " " ;
/** @var string Log-in user name for PostgreSQL database server (if using PostgreSQL) */
public $dbPostgreSQLUser = " arsse " ;
/** @var string Log-in password for PostgreSQL database server (if using PostgreSQL) */
public $dbPostgreSQLPass = " " ;
/** @var integer Listening port for PostgreSQL database server (if using PostgreSQL over TCP) */
public $dbPostgreSQLPort = 5432 ;
/** @var string Database name on PostgreSQL database server (if using PostgreSQL) */
public $dbPostgreSQLDb = " arsse " ;
2018-11-16 21:20:54 -05:00
/** @var string Schema name in PostgreSQL database (if using PostgreSQL) */
2018-11-10 00:02:38 -05:00
public $dbPostgreSQLSchema = " " ;
2018-11-16 21:20:54 -05:00
/** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */
public $dbPostgreSQLService = " " ;
2019-01-20 22:40:49 -05:00
/** @var string Host name or address of MySQL database server (if using MySQL) */
2018-12-20 18:06:28 -05:00
public $dbMySQLHost = " localhost " ;
2019-01-20 22:40:49 -05:00
/** @var string Log-in user name for MySQL database server (if using MySQL) */
2018-12-20 18:06:28 -05:00
public $dbMySQLUser = " arsse " ;
2019-01-20 22:40:49 -05:00
/** @var string Log-in password for MySQL database server (if using MySQL) */
2018-12-20 18:06:28 -05:00
public $dbMySQLPass = " " ;
2019-01-20 22:40:49 -05:00
/** @var integer Listening port for MySQL database server (if using MySQL over TCP) */
2018-12-20 18:06:28 -05:00
public $dbMySQLPort = 3306 ;
2019-01-20 22:40:49 -05:00
/** @var string Database name on MySQL database server (if using MySQL) */
2018-12-20 18:06:28 -05:00
public $dbMySQLDb = " arsse " ;
2019-01-15 08:58:11 -05:00
/** @var string Unix domain socket or named pipe to use for MySQL when not connecting over TCP */
public $dbMySQLSocket = " " ;
2016-10-15 09:45:23 -04:00
2019-01-20 22:40:49 -05:00
/** @var string The user management driver to use, currently only "internal". A fully-qualified class name may also be used for custom drivers */
public $userDriver = " internal " ;
2017-07-16 22:27:55 -04:00
/** @var boolean Whether users are already authenticated by the Web server before the application is executed */
2017-08-18 10:20:43 -04:00
public $userPreAuth = false ;
2018-10-26 14:40:20 -04:00
/** @var boolean Whether to require successful HTTP authentication before processing API-level authentication for protocols which have any. Normally the Tiny Tiny RSS relies on its own session-token authentication scheme, for example */
public $userHTTPAuthRequired = false ;
2017-07-16 22:27:55 -04:00
/** @var integer Desired length of temporary user passwords */
2017-02-20 17:04:13 -05:00
public $userTempPasswordLength = 20 ;
2018-10-26 14:40:20 -04:00
/** @var boolean Whether invalid or expired API session tokens should prevent logging in when HTTP authentication is used, for protocol which implement their own authentication */
public $userSessionEnforced = true ;
2019-01-20 22:40:49 -05:00
/** @ var \DateInterval Period of inactivity after which log - in sessions should be considered invalid , as an ISO 8601 duration ( default : 24 hours )
2017-09-16 19:57:33 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 14:40:20 -04:00
public $userSessionTimeout = " PT24H " ;
2019-01-20 22:40:49 -05:00
/** @ var \DateInterval Maximum lifetime of log - in sessions regardless of activity , as an ISO 8601 duration ( default : 7 days );
2017-09-16 19:57:33 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-01-01 12:31:42 -05:00
public $userSessionLifetime = " P7D " ;
2016-09-27 09:00:02 -04:00
2019-01-23 09:19:26 -05:00
/** @var string Feed update service driver to use, one of "serial" or "subprocess". A fully-qualified class name may also be used for custom drivers */
2019-01-20 22:40:49 -05:00
public $serviceDriver = " subprocess " ;
/** @ var \DateInterval The interval between checks for new articles , as an ISO 8601 duration
2017-07-27 09:09:39 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2017-07-11 20:27:37 -04:00
public $serviceFrequency = " PT2M " ;
2017-07-16 22:27:55 -04:00
/** @var integer Number of concurrent feed updates to perform */
2017-07-15 13:33:17 -04:00
public $serviceQueueWidth = 5 ;
2018-10-26 14:58:04 -04:00
2019-01-20 22:40:49 -05:00
/** @var \DateInterval Number of seconds to wait for data when fetching feeds from foreign servers */
2019-01-23 09:37:41 -05:00
public $fetchTimeout = 10.0 ;
2017-07-16 22:27:55 -04:00
/** @var integer Maximum size, in bytes, of data when fetching feeds from foreign servers */
2017-05-27 18:15:52 -04:00
public $fetchSizeLimit = 2 * 1024 * 1024 ;
2017-07-17 14:56:50 -04:00
/** @var boolean Whether to allow the possibility of fetching full article contents using an item's URL. Whether fetching will actually happen is also governed by a per-feed setting */
public $fetchEnableScraping = true ;
2017-08-02 18:27:04 -04:00
/** @var string|null User-Agent string to use when fetching feeds from foreign servers */
2019-01-20 22:40:49 -05:00
public $fetchUserAgentString = null ;
2016-09-27 09:00:02 -04:00
2019-01-20 22:40:49 -05:00
/** @ var \DateInterval | null When to delete a feed from the database after all its subscriptions have been deleted , as an ISO 8601 duration ( default : 24 hours ; null for never )
2017-08-02 18:27:04 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 14:40:20 -04:00
public $purgeFeeds = " PT24H " ;
2019-01-20 22:40:49 -05:00
/** @ var \DateInterval | null When to delete an unstarred article in the database after it has been marked read by all users , as an ISO 8601 duration ( default : 7 days ; null for never )
2017-08-17 22:36:15 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 14:40:20 -04:00
public $purgeArticlesRead = " P7D " ;
2019-01-20 22:40:49 -05:00
/** @ var \DateInterval | null When to delete an unstarred article in the database regardless of its read state , as an ISO 8601 duration ( default : 21 days ; null for never )
2017-08-17 22:36:15 -04:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2017-08-29 10:50:31 -04:00
public $purgeArticlesUnread = " P21D " ;
2017-08-02 18:27:04 -04:00
2018-01-11 15:48:29 -05:00
/** @var string Application name to present to clients during authentication */
public $httpRealm = " The Advanced RSS Environment " ;
2018-01-09 12:31:40 -05:00
/** @var string Space-separated list of origins from which to allow cross-origin resource sharing */
public $httpOriginsAllowed = " * " ;
/** @var string Space-separated list of origins from which to deny cross-origin resource sharing */
2018-01-11 15:48:29 -05:00
public $httpOriginsDenied = " " ;
2018-01-09 12:31:40 -05:00
2019-01-23 16:31:54 -05:00
### OBSOLETE SETTINGS
/** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
public $dbSQLite3Timeout = null ; // previously 60.0
2019-01-20 22:40:49 -05:00
const TYPE_NAMES = [
Value :: T_BOOL => " boolean " ,
Value :: T_STRING => " string " ,
Value :: T_FLOAT => " float " ,
VALUE :: T_INT => " integer " ,
Value :: T_INTERVAL => " interval " ,
];
2019-01-23 16:31:54 -05:00
const EXPECTED_TYPES = [
'dbTimeoutExec' => " double " ,
'dbTimeoutLock' => " double " ,
'dbTimeoutConnect' => " double " ,
'dbSQLite3Timeout' => " double " ,
];
2019-01-20 22:40:49 -05:00
protected static $types = [];
2017-07-16 22:27:55 -04:00
/** Creates a new configuration object
2017-07-27 09:09:39 -04:00
* @ param string $import_file Optional file to read configuration data from
* @ see self :: importFile () */
2017-02-16 15:29:42 -05:00
public function __construct ( string $import_file = " " ) {
2019-01-20 22:40:49 -05:00
if ( ! static :: $types ) {
static :: $types = $this -> propertyDiscover ();
}
foreach ( array_keys ( static :: $types ) as $prop ) {
$this -> $prop = $this -> propertyImport ( $prop , $this -> $prop );
}
2019-01-11 10:38:06 -05:00
if ( $import_file !== " " ) {
2017-07-21 17:15:43 -04:00
$this -> importFile ( $import_file );
}
2017-02-16 15:29:42 -05:00
}
2016-09-29 21:58:09 -04:00
2017-08-29 10:50:31 -04:00
/** Layers configuration data from a file into an existing object
2017-07-27 09:09:39 -04:00
*
2019-01-20 22:40:49 -05:00
* The file must be a PHP script which returns an array with keys that match the properties of the Conf class . Malformed files will throw an exception ; unknown keys are silently accepted . Files may be imported in succession , though this is not currently used .
2017-07-27 09:09:39 -04:00
* @ param string $file Full path and file name for the file to import */
2017-02-16 15:29:42 -05:00
public function importFile ( string $file ) : self {
2017-08-29 10:50:31 -04:00
if ( ! file_exists ( $file )) {
2017-07-21 17:15:43 -04:00
throw new Conf\Exception ( " fileMissing " , $file );
2017-08-29 10:50:31 -04:00
} elseif ( ! is_readable ( $file )) {
2017-07-21 17:15:43 -04:00
throw new Conf\Exception ( " fileUnreadable " , $file );
}
2017-02-16 15:29:42 -05:00
try {
ob_start ();
$arr = ( @ include $file );
2017-08-29 10:50:31 -04:00
} catch ( \Throwable $e ) {
2017-02-16 15:29:42 -05:00
$arr = null ;
} finally {
ob_end_clean ();
}
2017-08-29 10:50:31 -04:00
if ( ! is_array ( $arr )) {
2017-07-21 17:15:43 -04:00
throw new Conf\Exception ( " fileCorrupt " , $file );
}
2019-01-20 22:40:49 -05:00
return $this -> importData ( $arr , $file );
2017-02-16 15:29:42 -05:00
}
2016-09-27 09:00:02 -04:00
2017-08-29 10:50:31 -04:00
/** Layers configuration data from an associative array into an existing object
2017-07-27 09:09:39 -04:00
*
2019-01-20 22:40:49 -05:00
* The input array must have keys that match the properties of the Conf class ; unknown keys are silently accepted . Arrays may be imported in succession , though this is not currently used .
2017-07-27 09:09:39 -04:00
* @ param mixed [] $arr Array of configuration parameters to export */
2017-02-16 15:29:42 -05:00
public function import ( array $arr ) : self {
2019-01-20 22:40:49 -05:00
$file = debug_backtrace ( \DEBUG_BACKTRACE_IGNORE_ARGS , 1 )[ 0 ][ 'file' ] ? ? " " ;
return $this -> importData ( $arr , $file );
}
/** Layers configuration data from an associative array into an existing object */
protected function importData ( array $arr , string $file ) : self {
2017-08-29 10:50:31 -04:00
foreach ( $arr as $key => $value ) {
2019-01-20 22:40:49 -05:00
$this -> $key = $this -> propertyImport ( $key , $value , $file );
2017-02-16 15:29:42 -05:00
}
return $this ;
}
2016-09-27 09:00:02 -04:00
2017-07-27 09:09:39 -04:00
/** Outputs configuration settings , either non - default ones or all , as an associative array
* @ param bool $full Whether to output all configuration options rather than only changed ones */
public function export ( bool $full = false ) : array {
$conf = new \ReflectionObject ( $this );
2019-01-21 09:55:25 -05:00
$ref = ( new \ReflectionClass ( $this )) -> getDefaultProperties ();
$out = [];
2017-08-29 10:50:31 -04:00
foreach ( $conf -> getProperties ( \ReflectionProperty :: IS_PUBLIC ) as $prop ) {
2017-07-27 09:09:39 -04:00
$name = $prop -> name ;
2019-01-21 09:55:25 -05:00
$value = $prop -> getValue ( $this );
if ( $prop -> isDefault ()) {
$default = $ref [ $name ];
// if the property is a known property (rather than one added by a hypothetical plug-in)
// we convert intervals to strings and then export anything which doesn't match the default value
$value = $this -> propertyExport ( $name , $value );
if (( is_scalar ( $value ) || is_null ( $value )) && ( $full || $value !== $ref [ $name ])) {
$out [ $name ] = $value ;
}
} elseif ( is_scalar ( $value ) || is_null ( $value )) {
// otherwise export the property only if it is scalar
$out [ $name ] = $value ;
2017-07-27 09:09:39 -04:00
}
}
return $out ;
2017-02-16 15:29:42 -05:00
}
2017-07-27 09:09:39 -04:00
/** Outputs configuration settings , either non - default ones or all , to a file in a format suitable for later import
* @ param string $file Full path and file name for the file to import to ; the containing directory must already exist
* @ param bool $full Whether to output all configuration options rather than only changed ones */
public function exportFile ( string $file , bool $full = false ) : bool {
$arr = $this -> export ( $full );
$conf = new \ReflectionObject ( $this );
$out = " <?php return [ " . PHP_EOL ;
2017-08-29 10:50:31 -04:00
foreach ( $arr as $prop => $value ) {
2017-07-27 09:09:39 -04:00
$match = null ;
$doc = $comment = " " ;
// retrieve the property's docblock, if it exists
try {
$doc = ( new \ReflectionProperty ( self :: class , $prop )) -> getDocComment ();
// parse the docblock to extract the property description
2019-01-21 09:55:25 -05:00
if ( preg_match ( " <@var \ s+ \ S+ \ s+(.+?)(?: \ s* \ */)? \ s* $ >m " , $doc , $match )) {
2017-07-27 09:09:39 -04:00
$comment = $match [ 1 ];
}
2019-01-21 09:55:25 -05:00
} catch ( \ReflectionException $e ) {
2017-07-27 09:09:39 -04:00
}
// append the docblock description if there is one, or an empty comment otherwise
$out .= " // " . $comment . PHP_EOL ;
// append the property and an export of its value to the output
2017-08-29 10:50:31 -04:00
$out .= " " . var_export ( $prop , true ) . " => " . var_export ( $value , true ) . " , " . PHP_EOL ;
2017-07-27 09:09:39 -04:00
}
$out .= " ]; " . PHP_EOL ;
// write the configuration representation to the requested file
2017-08-29 10:50:31 -04:00
if ( !@ file_put_contents ( $file , $out )) {
2017-07-27 09:09:39 -04:00
// if it fails throw an exception
$err = file_exists ( $file ) ? " fileUnwritable " : " fileUncreatable " ;
throw new Conf\Exception ( $err , $file );
}
return true ;
2017-02-16 15:29:42 -05:00
}
2019-01-20 22:40:49 -05:00
/** Caches information about configuration properties for later access */
protected function propertyDiscover () : array {
$out = [];
$rc = new \ReflectionClass ( $this );
foreach ( $rc -> getProperties ( \ReflectionProperty :: IS_PUBLIC ) as $p ) {
if ( preg_match ( " /@var \ s+((?:int(eger)?|float|bool(ean)?|string| \\ \\ DateInterval)(?: \ |null)?)[^ \ []/ " , $p -> getDocComment (), $match )) {
$match = explode ( " | " , $match [ 1 ]);
$nullable = ( sizeof ( $match ) > 1 );
$type = [
'string' => Value :: T_STRING | Value :: M_STRICT ,
'integer' => Value :: T_INT | Value :: M_STRICT ,
'boolean' => Value :: T_BOOL | Value :: M_STRICT ,
'float' => Value :: T_FLOAT | Value :: M_STRICT ,
'\\DateInterval' => Value :: T_INTERVAL | Value :: M_LOOSE ,
][ $match [ 0 ]];
if ( $nullable ) {
$type |= Value :: M_NULL ;
}
} else {
$type = Value :: T_MIXED ; // @codeCoverageIgnore
}
$out [ $p -> name ] = [ 'name' => $match [ 0 ], 'const' => $type ];
}
return $out ;
}
protected function propertyImport ( string $key , $value , string $file = " " ) {
2019-01-23 16:31:54 -05:00
$typeName = static :: $types [ $key ][ 'name' ] ? ? " mixed " ;
$typeConst = static :: $types [ $key ][ 'const' ] ? ? Value :: T_MIXED ;
$nullable = ( int ) ( bool ) ( static :: $types [ $key ][ 'const' ] & Value :: M_NULL );
2019-01-20 22:40:49 -05:00
try {
if ( $typeName === " \\ DateInterval " ) {
// date intervals have special handling: if the existing value (ultimately, the default value)
// is an integer or float, the new value should be imported as numeric. If the new value is a string
// it is first converted to an interval and then converted to the numeric type if necessary
2019-01-23 16:31:54 -05:00
$mode = $nullable ? Value :: M_STRICT | Value :: M_NULL : Value :: M_STRICT ;
2019-01-20 22:40:49 -05:00
if ( is_string ( $value )) {
2019-01-23 16:31:54 -05:00
$value = Value :: normalize ( $value , Value :: T_INTERVAL | $mode );
2019-01-20 22:40:49 -05:00
}
2019-01-23 16:31:54 -05:00
switch ( self :: EXPECTED_TYPES [ $key ] ? ? gettype ( $this -> $key )) {
2019-01-20 22:40:49 -05:00
case " integer " :
2019-01-25 22:07:37 -05:00
return Value :: normalize ( $value , Value :: T_INT | $mode ); // @codeCoverageIgnore
2019-01-20 22:40:49 -05:00
case " double " :
2019-01-23 16:31:54 -05:00
return Value :: normalize ( $value , Value :: T_FLOAT | $mode );
2019-01-20 22:40:49 -05:00
case " string " :
case " object " :
return $value ;
default :
2019-01-23 16:31:54 -05:00
throw new Conf\Exception ( " ambiguousDefault " , [ 'param' => $key ]); // @codeCoverageIgnore
2019-01-20 22:40:49 -05:00
}
}
$value = Value :: normalize ( $value , $typeConst );
switch ( $key ) {
case " dbDriver " :
$driver = $driver ? ? Database :: DRIVER_NAMES [ strtolower ( $value )] ? ? $value ;
$interface = $interface ? ? Db\Driver :: class ;
// no break
case " userDriver " :
$driver = $driver ? ? User :: DRIVER_NAMES [ strtolower ( $value )] ? ? $value ;
$interface = $interface ? ? User\Driver :: class ;
// no break
case " serviceDriver " :
$driver = $driver ? ? Service :: DRIVER_NAMES [ strtolower ( $value )] ? ? $value ;
$interface = $interface ? ? Service\Driver :: class ;
if ( ! is_subclass_of ( $driver , $interface )) {
throw new Conf\Exception ( " semanticMismatch " , [ 'param' => $key , 'file' => $file ]);
}
return $driver ;
}
return $value ;
} catch ( ExceptionType $e ) {
$type = static :: $types [ $key ][ 'const' ] & ~ ( Value :: M_STRICT | Value :: M_DROP | Value :: M_NULL | Value :: M_ARRAY );
throw new Conf\Exception ( " typeMismatch " , [ 'param' => $key , 'type' => self :: TYPE_NAMES [ $type ], 'file' => $file , 'nullable' => $nullable ]);
}
}
2019-01-21 09:55:25 -05:00
protected function propertyExport ( string $key , $value ) {
$value = ( $value instanceof \DateInterval ) ? Value :: normalize ( $value , Value :: T_STRING ) : $value ;
switch ( $key ) {
case " dbDriver " :
return array_flip ( Database :: DRIVER_NAMES )[ $value ] ? ? $value ;
case " userDriver " :
return array_flip ( User :: DRIVER_NAMES )[ $value ] ? ? $value ;
case " serviceDriver " :
return array_flip ( Service :: DRIVER_NAMES )[ $value ] ? ? $value ;
default :
return $value ;
}
}
2017-08-29 10:50:31 -04:00
}