2016-09-27 13:00:02 +00:00
< ? php
2017-11-17 01:23:18 +00:00
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
2017-07-17 02:27:55 +00:00
/** Conf class */
2016-09-27 13:00:02 +00:00
declare ( strict_types = 1 );
2017-03-28 04:12:12 +00:00
namespace JKingWeb\Arsse ;
2016-09-27 13:00:02 +00:00
2019-01-21 03:40:49 +00:00
use JKingWeb\Arsse\Misc\ValueInfo as Value ;
2017-07-17 02:27:55 +00:00
/** Class for loading , saving , and querying configuration
2017-08-29 14:50:31 +00:00
*
2017-07-27 13:09:39 +00: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 13:00:02 +00:00
class Conf {
2017-07-17 02:27:55 +00:00
/** @var string Default language to use for logging and errors */
2017-02-16 20:29:42 +00:00
public $lang = " en " ;
2016-10-15 13:45:23 +00:00
2019-01-21 03:40:49 +00: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-12 00:27:37 +00:00
public $dbAutoUpdate = true ;
2019-01-21 03:40:49 +00:00
/** @var \DateInterval Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */
2018-11-22 18:30:13 +00:00
public $dbTimeoutConnect = 5.0 ;
2019-01-21 03:40:49 +00:00
/** @var \DateInterval Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */
2018-11-22 18:30:13 +00:00
public $dbTimeoutExec = 0.0 ;
2017-08-28 23:38:58 +00:00
/** @var string|null Full path and file name of SQLite database (if using SQLite) */
public $dbSQLite3File = null ;
2017-07-17 02:27:55 +00:00
/** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */
2017-02-16 20:29:42 +00:00
public $dbSQLite3Key = " " ;
2019-01-21 03:40:49 +00:00
/** @var \DateInterval 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) */
2018-11-22 18:30:13 +00:00
public $dbSQLite3Timeout = 60.0 ;
2018-11-10 05:02:38 +00: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-17 02:20:54 +00:00
/** @var string Schema name in PostgreSQL database (if using PostgreSQL) */
2018-11-10 05:02:38 +00:00
public $dbPostgreSQLSchema = " " ;
2018-11-17 02:20:54 +00: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-21 03:40:49 +00:00
/** @var string Host name or address of MySQL database server (if using MySQL) */
2018-12-20 23:06:28 +00:00
public $dbMySQLHost = " localhost " ;
2019-01-21 03:40:49 +00:00
/** @var string Log-in user name for MySQL database server (if using MySQL) */
2018-12-20 23:06:28 +00:00
public $dbMySQLUser = " arsse " ;
2019-01-21 03:40:49 +00:00
/** @var string Log-in password for MySQL database server (if using MySQL) */
2018-12-20 23:06:28 +00:00
public $dbMySQLPass = " " ;
2019-01-21 03:40:49 +00:00
/** @var integer Listening port for MySQL database server (if using MySQL over TCP) */
2018-12-20 23:06:28 +00:00
public $dbMySQLPort = 3306 ;
2019-01-21 03:40:49 +00:00
/** @var string Database name on MySQL database server (if using MySQL) */
2018-12-20 23:06:28 +00:00
public $dbMySQLDb = " arsse " ;
2019-01-15 13:58:11 +00:00
/** @var string Unix domain socket or named pipe to use for MySQL when not connecting over TCP */
public $dbMySQLSocket = " " ;
2016-10-15 13:45:23 +00:00
2019-01-21 03:40:49 +00: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-17 02:27:55 +00:00
/** @var boolean Whether users are already authenticated by the Web server before the application is executed */
2017-08-18 14:20:43 +00:00
public $userPreAuth = false ;
2018-10-26 18:40:20 +00: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-17 02:27:55 +00:00
/** @var integer Desired length of temporary user passwords */
2017-02-20 22:04:13 +00:00
public $userTempPasswordLength = 20 ;
2018-10-26 18:40:20 +00: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-21 03:40:49 +00: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 23:57:33 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 18:40:20 +00:00
public $userSessionTimeout = " PT24H " ;
2019-01-21 03:40:49 +00:00
/** @ var \DateInterval Maximum lifetime of log - in sessions regardless of activity , as an ISO 8601 duration ( default : 7 days );
2017-09-16 23:57:33 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-01-01 17:31:42 +00:00
public $userSessionLifetime = " P7D " ;
2016-09-27 13:00:02 +00:00
2019-01-21 03:40:49 +00:00
/** @var string Feed update service driver to use, one of "serial", "subprocess", or "curl". A fully-qualified class name may also be used for custom drivers */
public $serviceDriver = " subprocess " ;
/** @ var \DateInterval The interval between checks for new articles , as an ISO 8601 duration
2017-07-27 13:09:39 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2017-07-12 00:27:37 +00:00
public $serviceFrequency = " PT2M " ;
2017-07-17 02:27:55 +00:00
/** @var integer Number of concurrent feed updates to perform */
2017-07-15 17:33:17 +00:00
public $serviceQueueWidth = 5 ;
2017-07-17 02:27:55 +00:00
/** @var string The base server address (with scheme, host, port if necessary, and terminal slash) to connect to the server when performing feed updates using cURL */
2017-07-12 00:27:37 +00:00
public $serviceCurlBase = " http://localhost/ " ;
2019-01-21 03:40:49 +00:00
/** @var string The user name to use when performing feed updates using cURL */
public $serviceCurlUser = " " ;
2017-07-17 02:27:55 +00:00
/** @var string The password to use when performing feed updates using cURL */
2019-01-21 03:40:49 +00:00
public $serviceCurlPassword = " " ;
2018-10-26 18:58:04 +00:00
2019-01-21 03:40:49 +00:00
/** @var \DateInterval Number of seconds to wait for data when fetching feeds from foreign servers */
2017-05-27 22:15:52 +00:00
public $fetchTimeout = 10 ;
2017-07-17 02:27:55 +00:00
/** @var integer Maximum size, in bytes, of data when fetching feeds from foreign servers */
2017-05-27 22:15:52 +00:00
public $fetchSizeLimit = 2 * 1024 * 1024 ;
2017-07-17 18:56:50 +00: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 22:27:04 +00:00
/** @var string|null User-Agent string to use when fetching feeds from foreign servers */
2019-01-21 03:40:49 +00:00
public $fetchUserAgentString = null ;
2016-09-27 13:00:02 +00:00
2019-01-21 03:40:49 +00: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 22:27:04 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 18:40:20 +00:00
public $purgeFeeds = " PT24H " ;
2019-01-21 03:40:49 +00: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-18 02:36:15 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2018-10-26 18:40:20 +00:00
public $purgeArticlesRead = " P7D " ;
2019-01-21 03:40:49 +00: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-18 02:36:15 +00:00
* @ see https :// en . wikipedia . org / wiki / ISO_8601 #Durations */
2017-08-29 14:50:31 +00:00
public $purgeArticlesUnread = " P21D " ;
2017-08-02 22:27:04 +00:00
2018-01-11 20:48:29 +00:00
/** @var string Application name to present to clients during authentication */
public $httpRealm = " The Advanced RSS Environment " ;
2018-01-09 17:31:40 +00: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 20:48:29 +00:00
public $httpOriginsDenied = " " ;
2018-01-09 17:31:40 +00:00
2019-01-21 03:40:49 +00:00
const TYPE_NAMES = [
Value :: T_BOOL => " boolean " ,
Value :: T_STRING => " string " ,
Value :: T_FLOAT => " float " ,
VALUE :: T_INT => " integer " ,
Value :: T_INTERVAL => " interval " ,
];
protected static $types = [];
2017-07-17 02:27:55 +00:00
/** Creates a new configuration object
2017-07-27 13:09:39 +00:00
* @ param string $import_file Optional file to read configuration data from
* @ see self :: importFile () */
2017-02-16 20:29:42 +00:00
public function __construct ( string $import_file = " " ) {
2019-01-21 03:40:49 +00: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 15:38:06 +00:00
if ( $import_file !== " " ) {
2017-07-21 21:15:43 +00:00
$this -> importFile ( $import_file );
}
2017-02-16 20:29:42 +00:00
}
2016-09-30 01:58:09 +00:00
2017-08-29 14:50:31 +00:00
/** Layers configuration data from a file into an existing object
2017-07-27 13:09:39 +00:00
*
2019-01-21 03:40:49 +00: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 13:09:39 +00:00
* @ param string $file Full path and file name for the file to import */
2017-02-16 20:29:42 +00:00
public function importFile ( string $file ) : self {
2017-08-29 14:50:31 +00:00
if ( ! file_exists ( $file )) {
2017-07-21 21:15:43 +00:00
throw new Conf\Exception ( " fileMissing " , $file );
2017-08-29 14:50:31 +00:00
} elseif ( ! is_readable ( $file )) {
2017-07-21 21:15:43 +00:00
throw new Conf\Exception ( " fileUnreadable " , $file );
}
2017-02-16 20:29:42 +00:00
try {
ob_start ();
$arr = ( @ include $file );
2017-08-29 14:50:31 +00:00
} catch ( \Throwable $e ) {
2017-02-16 20:29:42 +00:00
$arr = null ;
} finally {
ob_end_clean ();
}
2017-08-29 14:50:31 +00:00
if ( ! is_array ( $arr )) {
2017-07-21 21:15:43 +00:00
throw new Conf\Exception ( " fileCorrupt " , $file );
}
2019-01-21 03:40:49 +00:00
return $this -> importData ( $arr , $file );
2017-02-16 20:29:42 +00:00
}
2016-09-27 13:00:02 +00:00
2017-08-29 14:50:31 +00:00
/** Layers configuration data from an associative array into an existing object
2017-07-27 13:09:39 +00:00
*
2019-01-21 03:40:49 +00: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 13:09:39 +00:00
* @ param mixed [] $arr Array of configuration parameters to export */
2017-02-16 20:29:42 +00:00
public function import ( array $arr ) : self {
2019-01-21 03:40:49 +00: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 14:50:31 +00:00
foreach ( $arr as $key => $value ) {
2019-01-21 03:40:49 +00:00
$this -> $key = $this -> propertyImport ( $key , $value , $file );
2017-02-16 20:29:42 +00:00
}
return $this ;
}
2016-09-27 13:00:02 +00:00
2017-07-27 13:09:39 +00: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 {
$ref = new self ;
$out = [];
$conf = new \ReflectionObject ( $this );
2017-08-29 14:50:31 +00:00
foreach ( $conf -> getProperties ( \ReflectionProperty :: IS_PUBLIC ) as $prop ) {
2017-07-27 13:09:39 +00:00
$name = $prop -> name ;
2019-01-21 03:40:49 +00:00
// add the property to the output if the value is of a supported type and either:
2017-07-27 13:09:39 +00:00
// 1. full output has been requested
// 2. the property is not defined in the class
// 3. it differs from the default
2017-08-30 03:17:57 +00:00
if (( is_scalar ( $this -> $name ) || is_null ( $this -> $name )) && ( $full || ! $prop -> isDefault () || $this -> $name !== $ref -> $name )) {
2017-07-27 13:09:39 +00:00
$out [ $name ] = $this -> $name ;
}
}
return $out ;
2017-02-16 20:29:42 +00:00
}
2017-07-27 13:09:39 +00: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 14:50:31 +00:00
foreach ( $arr as $prop => $value ) {
2017-07-27 13:09:39 +00:00
$match = null ;
$doc = $comment = " " ;
// retrieve the property's docblock, if it exists
try {
$doc = ( new \ReflectionProperty ( self :: class , $prop )) -> getDocComment ();
2017-08-29 14:50:31 +00:00
} catch ( \ReflectionException $e ) {
}
if ( $doc ) {
2017-07-27 13:09:39 +00:00
// parse the docblock to extract the property description
2017-08-29 14:50:31 +00:00
if ( preg_match ( " <@var \ s+ \ S+ \ s+(.+?)(?: \ s* \ */)? $ >m " , $doc , $match )) {
2017-07-27 13:09:39 +00:00
$comment = $match [ 1 ];
}
}
// 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 14:50:31 +00:00
$out .= " " . var_export ( $prop , true ) . " => " . var_export ( $value , true ) . " , " . PHP_EOL ;
2017-07-27 13:09:39 +00:00
}
$out .= " ]; " . PHP_EOL ;
// write the configuration representation to the requested file
2017-08-29 14:50:31 +00:00
if ( !@ file_put_contents ( $file , $out )) {
2017-07-27 13:09:39 +00:00
// if it fails throw an exception
$err = file_exists ( $file ) ? " fileUnwritable " : " fileUncreatable " ;
throw new Conf\Exception ( $err , $file );
}
return true ;
2017-02-16 20:29:42 +00:00
}
2019-01-21 03:40:49 +00: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 = " " ) {
try {
$typeName = static :: $types [ $key ][ 'name' ] ? ? " mixed " ;
$typeConst = static :: $types [ $key ][ 'const' ] ? ? Value :: T_MIXED ;
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
if ( is_string ( $value )) {
$value = Value :: normalize ( $value , Value :: T_INTERVAL | Value :: M_STRICT );
}
switch ( gettype ( $this -> $key )) {
case " integer " :
return Value :: normalize ( $value , Value :: T_INT | Value :: M_STRICT );
case " double " :
return Value :: normalize ( $value , Value :: T_FLOAT | Value :: M_STRICT );
case " string " :
case " object " :
return $value ;
default :
throw new ExceptionType ( " strictFailure " ); // @codeCoverageIgnore
}
}
$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 ) {
$nullable = ( int ) ( bool ) ( static :: $types [ $key ] & Value :: M_NULL );
$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 ]);
}
}
2017-08-29 14:50:31 +00:00
}