mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Validate configuration parameters on import, and other changes
- Each parameter is checked for type and normalized - Interval strings are converted to DateInterval objects - Timeouts can be specified as interval strings - Most intervals can be null to signify infinity - Driver classes are checked that they implement the correct interface - Short driver names may be used, and are used by default - Helpful errors messages are printed in case of erroneous configuration Exporting is currently broken; this will be fixed in an upcoming commit
This commit is contained in:
parent
b0643de21c
commit
5cd84c4ab4
22 changed files with 280 additions and 178 deletions
|
@ -19,6 +19,7 @@ abstract class AbstractException extends \Exception {
|
|||
"Lang/Exception.fileCorrupt" => 10104,
|
||||
"Lang/Exception.stringMissing" => 10105,
|
||||
"Lang/Exception.stringInvalid" => 10106,
|
||||
"Lang/Exception.dataInvalid" => 10107,
|
||||
"Db/Exception.extMissing" => 10201,
|
||||
"Db/Exception.fileMissing" => 10202,
|
||||
"Db/Exception.fileUnusable" => 10203,
|
||||
|
@ -62,6 +63,8 @@ abstract class AbstractException extends \Exception {
|
|||
"Conf/Exception.fileUnwritable" => 10304,
|
||||
"Conf/Exception.fileUncreatable" => 10305,
|
||||
"Conf/Exception.fileCorrupt" => 10306,
|
||||
"Conf/Exception.typeMismatch" => 10311,
|
||||
"Conf/Exception.semanticMismatch" => 10312,
|
||||
"User/Exception.functionNotImplemented" => 10401,
|
||||
"User/Exception.doesNotExist" => 10402,
|
||||
"User/Exception.alreadyExists" => 10403,
|
||||
|
|
161
lib/Conf.php
161
lib/Conf.php
|
@ -7,6 +7,8 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse;
|
||||
|
||||
use JKingWeb\Arsse\Misc\ValueInfo as Value;
|
||||
|
||||
/** Class for loading, saving, and querying configuration
|
||||
*
|
||||
* 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.
|
||||
|
@ -15,19 +17,19 @@ class Conf {
|
|||
/** @var string Default language to use for logging and errors */
|
||||
public $lang = "en";
|
||||
|
||||
/** @var string Class of the database driver in use (SQLite3 by default) */
|
||||
public $dbDriver = Db\SQLite3\Driver::class;
|
||||
/** @var boolean Whether to attempt to automatically update the database when updated to a new version with schema changes */
|
||||
/** @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 */
|
||||
public $dbAutoUpdate = true;
|
||||
/** @var float Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */
|
||||
/** @var \DateInterval Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */
|
||||
public $dbTimeoutConnect = 5.0;
|
||||
/** @var float Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */
|
||||
/** @var \DateInterval Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */
|
||||
public $dbTimeoutExec = 0.0;
|
||||
/** @var string|null Full path and file name of SQLite database (if using SQLite) */
|
||||
public $dbSQLite3File = null;
|
||||
/** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */
|
||||
public $dbSQLite3Key = "";
|
||||
/** @var float 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) */
|
||||
/** @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) */
|
||||
public $dbSQLite3Timeout = 60.0;
|
||||
/** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */
|
||||
public $dbPostgreSQLHost = "";
|
||||
|
@ -43,21 +45,21 @@ class Conf {
|
|||
public $dbPostgreSQLSchema = "";
|
||||
/** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */
|
||||
public $dbPostgreSQLService = "";
|
||||
/** @var string Host name, address, or socket path of MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
/** @var string Host name or address of MySQL database server (if using MySQL) */
|
||||
public $dbMySQLHost = "localhost";
|
||||
/** @var string Log-in user name for MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
/** @var string Log-in user name for MySQL database server (if using MySQL) */
|
||||
public $dbMySQLUser = "arsse";
|
||||
/** @var string Log-in password for MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
/** @var string Log-in password for MySQL database server (if using MySQL) */
|
||||
public $dbMySQLPass = "";
|
||||
/** @var integer Listening port for MySQL/MariaDB database server (if using MySQL/MariaDB over TCP) */
|
||||
/** @var integer Listening port for MySQL database server (if using MySQL over TCP) */
|
||||
public $dbMySQLPort = 3306;
|
||||
/** @var string Database name on MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
/** @var string Database name on MySQL database server (if using MySQL) */
|
||||
public $dbMySQLDb = "arsse";
|
||||
/** @var string Unix domain socket or named pipe to use for MySQL when not connecting over TCP */
|
||||
public $dbMySQLSocket = "";
|
||||
|
||||
/** @var string Class of the user management driver in use (Internal by default) */
|
||||
public $userDriver = User\Internal\Driver::class;
|
||||
/** @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";
|
||||
/** @var boolean Whether users are already authenticated by the Web server before the application is executed */
|
||||
public $userPreAuth = false;
|
||||
/** @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 */
|
||||
|
@ -66,43 +68,43 @@ class Conf {
|
|||
public $userTempPasswordLength = 20;
|
||||
/** @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;
|
||||
/** @var string Period of inactivity after which log-in sessions should be considered invalid, as an ISO 8601 duration (default: 24 hours)
|
||||
/** @var \DateInterval Period of inactivity after which log-in sessions should be considered invalid, as an ISO 8601 duration (default: 24 hours)
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $userSessionTimeout = "PT24H";
|
||||
/** @var string Maximum lifetime of log-in sessions regardless of activity, as an ISO 8601 duration (default: 7 days);
|
||||
/** @var \DateInterval Maximum lifetime of log-in sessions regardless of activity, as an ISO 8601 duration (default: 7 days);
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $userSessionLifetime = "P7D";
|
||||
|
||||
/** @var string Class of the background feed update service driver in use (Forking by default) */
|
||||
public $serviceDriver = Service\Forking\Driver::class;
|
||||
/** @var string The interval between checks for new articles, as an ISO 8601 duration
|
||||
/** @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
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $serviceFrequency = "PT2M";
|
||||
/** @var integer Number of concurrent feed updates to perform */
|
||||
public $serviceQueueWidth = 5;
|
||||
/** @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 */
|
||||
public $serviceCurlBase = "http://localhost/";
|
||||
/** @var string The user name to use when performing feed updates using cURL; if none is provided, a temporary name and password will be stored in the database (this is not compatible with pre-authentication) */
|
||||
public $serviceCurlUser = null;
|
||||
/** @var string The user name to use when performing feed updates using cURL */
|
||||
public $serviceCurlUser = "";
|
||||
/** @var string The password to use when performing feed updates using cURL */
|
||||
public $serviceCurlPassword = null;
|
||||
public $serviceCurlPassword = "";
|
||||
|
||||
/** @var integer Number of seconds to wait for data when fetching feeds from foreign servers */
|
||||
/** @var \DateInterval Number of seconds to wait for data when fetching feeds from foreign servers */
|
||||
public $fetchTimeout = 10;
|
||||
/** @var integer Maximum size, in bytes, of data when fetching feeds from foreign servers */
|
||||
public $fetchSizeLimit = 2 * 1024 * 1024;
|
||||
/** @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;
|
||||
/** @var string|null User-Agent string to use when fetching feeds from foreign servers */
|
||||
public $fetchUserAgentString;
|
||||
public $fetchUserAgentString = null;
|
||||
|
||||
/** @var string When to delete a feed from the database after all its subscriptions have been deleted, as an ISO 8601 duration (default: 24 hours; empty string for never)
|
||||
/** @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)
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $purgeFeeds = "PT24H";
|
||||
/** @var string 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; empty string for never)
|
||||
/** @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)
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $purgeArticlesRead = "P7D";
|
||||
/** @var string When to delete an unstarred article in the database regardless of its read state, as an ISO 8601 duration (default: 21 days; empty string for never)
|
||||
/** @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)
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $purgeArticlesUnread = "P21D";
|
||||
|
||||
|
@ -113,10 +115,26 @@ class Conf {
|
|||
/** @var string Space-separated list of origins from which to deny cross-origin resource sharing */
|
||||
public $httpOriginsDenied = "";
|
||||
|
||||
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 = [];
|
||||
|
||||
/** Creates a new configuration object
|
||||
* @param string $import_file Optional file to read configuration data from
|
||||
* @see self::importFile() */
|
||||
public function __construct(string $import_file = "") {
|
||||
if (!static::$types) {
|
||||
static::$types = $this->propertyDiscover();
|
||||
}
|
||||
foreach (array_keys(static::$types) as $prop) {
|
||||
$this->$prop = $this->propertyImport($prop, $this->$prop);
|
||||
}
|
||||
if ($import_file !== "") {
|
||||
$this->importFile($import_file);
|
||||
}
|
||||
|
@ -124,7 +142,7 @@ class Conf {
|
|||
|
||||
/** Layers configuration data from a file into an existing object
|
||||
*
|
||||
* The file must be a PHP script which return an array with keys that match the properties of the Conf class. Malformed files will throw an exception; unknown keys are silently ignored. Files may be imported is succession, though this is not currently used.
|
||||
* 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.
|
||||
* @param string $file Full path and file name for the file to import */
|
||||
public function importFile(string $file): self {
|
||||
if (!file_exists($file)) {
|
||||
|
@ -143,16 +161,22 @@ class Conf {
|
|||
if (!is_array($arr)) {
|
||||
throw new Conf\Exception("fileCorrupt", $file);
|
||||
}
|
||||
return $this->import($arr);
|
||||
return $this->importData($arr, $file);
|
||||
}
|
||||
|
||||
/** Layers configuration data from an associative array into an existing object
|
||||
*
|
||||
* The input array must have keys that match the properties of the Conf class; unknown keys are silently ignored. Arrays may be imported is succession, though this is not currently used.
|
||||
* 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.
|
||||
* @param mixed[] $arr Array of configuration parameters to export */
|
||||
public function import(array $arr): self {
|
||||
$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 {
|
||||
foreach ($arr as $key => $value) {
|
||||
$this->$key = $value;
|
||||
$this->$key = $this->propertyImport($key, $value, $file);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
@ -165,7 +189,7 @@ class Conf {
|
|||
$conf = new \ReflectionObject($this);
|
||||
foreach ($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
|
||||
$name = $prop->name;
|
||||
// add the property to the output if the value is scalar (or null) and either:
|
||||
// add the property to the output if the value is of a supported type and either:
|
||||
// 1. full output has been requested
|
||||
// 2. the property is not defined in the class
|
||||
// 3. it differs from the default
|
||||
|
@ -211,4 +235,79 @@ class Conf {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,11 @@ use JKingWeb\Arsse\Misc\ValueInfo;
|
|||
class Database {
|
||||
const SCHEMA_VERSION = 4;
|
||||
const LIMIT_ARTICLES = 50;
|
||||
const DRIVER_NAMES = [
|
||||
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
|
||||
'postgresql' => \JKingWeb\Arsse\Db\PostgreSQL\Driver::class,
|
||||
'mysql' => \JKingWeb\Arsse\Db\MySQL\Driver::class,
|
||||
];
|
||||
|
||||
/** @var Db\Driver */
|
||||
public $db;
|
||||
|
@ -760,14 +765,13 @@ class Database {
|
|||
$this->db->query("UPDATE arsse_feeds set orphaned = null where exists(SELECT id from arsse_subscriptions where feed = arsse_feeds.id)");
|
||||
// next mark any newly orphaned feeds with the current date and time
|
||||
$this->db->query("UPDATE arsse_feeds set orphaned = CURRENT_TIMESTAMP where orphaned is null and not exists(SELECT id from arsse_subscriptions where feed = arsse_feeds.id)");
|
||||
// finally delete feeds that have been orphaned longer than the retention period
|
||||
$limit = Date::normalize("now");
|
||||
// finally delete feeds that have been orphaned longer than the retention period, if a a purge threshold has been specified
|
||||
if (Arsse::$conf->purgeFeeds) {
|
||||
// if there is a retention period specified, compute it; otherwise feed are deleted immediatelty
|
||||
$limit = Date::sub(Arsse::$conf->purgeFeeds, $limit);
|
||||
$limit = Date::sub(Arsse::$conf->purgeFeeds);
|
||||
$out = (bool) $this->db->prepare("DELETE from arsse_feeds where orphaned <= ?", "datetime")->run($limit);
|
||||
} else {
|
||||
$out = false;
|
||||
}
|
||||
$out = (bool) $this->db->prepare("DELETE from arsse_feeds where orphaned <= ?", "datetime")->run($limit);
|
||||
// commit changes and return
|
||||
$tr->commit();
|
||||
return $out;
|
||||
}
|
||||
|
|
|
@ -28,12 +28,12 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
if (!static::requirementsMet()) {
|
||||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
$host = strtolower(!strlen((string) Arsse::$conf->dbMySQLHost) ? "localhost" : Arsse::$conf->dbMySQLHost);
|
||||
$socket = strlen((string) Arsse::$conf->dbMySQLSocket) ? Arsse::$conf->dbMySQLSocket : ini_get("mysqli.default_socket");
|
||||
$user = Arsse::$conf->dbMySQLUser ?? "";
|
||||
$pass = Arsse::$conf->dbMySQLPass ?? "";
|
||||
$port = Arsse::$conf->dbMySQLPost ?? 3306;
|
||||
$db = Arsse::$conf->dbMySQLDb ?? "arsse";
|
||||
$host = strtolower(!strlen(Arsse::$conf->dbMySQLHost) ? "localhost" : Arsse::$conf->dbMySQLHost);
|
||||
$socket = strlen(Arsse::$conf->dbMySQLSocket) ? Arsse::$conf->dbMySQLSocket : ini_get("mysqli.default_socket");
|
||||
$user = Arsse::$conf->dbMySQLUser;
|
||||
$pass = Arsse::$conf->dbMySQLPass;
|
||||
$port = Arsse::$conf->dbMySQLPort;
|
||||
$db = Arsse::$conf->dbMySQLDb;
|
||||
// make the connection
|
||||
$this->makeConnection($db, $user, $pass, $host, $port, $socket);
|
||||
// set session variables
|
||||
|
|
|
@ -25,13 +25,13 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
if (!static::requirementsMet()) {
|
||||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
$user = $user ?? Arsse::$conf->dbPostgreSQLUser;
|
||||
$pass = $pass ?? Arsse::$conf->dbPostgreSQLPass;
|
||||
$db = $db ?? Arsse::$conf->dbPostgreSQLDb;
|
||||
$host = $host ?? Arsse::$conf->dbPostgreSQLHost;
|
||||
$port = $port ?? Arsse::$conf->dbPostgreSQLPort;
|
||||
$schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema;
|
||||
$service = $service ?? Arsse::$conf->dbPostgreSQLService;
|
||||
$user = Arsse::$conf->dbPostgreSQLUser;
|
||||
$pass = Arsse::$conf->dbPostgreSQLPass;
|
||||
$db = Arsse::$conf->dbPostgreSQLDb;
|
||||
$host = Arsse::$conf->dbPostgreSQLHost;
|
||||
$port = Arsse::$conf->dbPostgreSQLPort;
|
||||
$schema = Arsse::$conf->dbPostgreSQLSchema;
|
||||
$service = Arsse::$conf->dbPostgreSQLService;
|
||||
$this->makeConnection($user, $pass, $db, $host, $port, $service);
|
||||
foreach (static::makeSetupQueries($schema) as $q) {
|
||||
$this->exec($q);
|
||||
|
@ -42,7 +42,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
$base = [
|
||||
'client_encoding' => "UTF8",
|
||||
'application_name' => "arsse",
|
||||
'connect_timeout' => (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0),
|
||||
'connect_timeout' => (string) ceil(Arsse::$conf->dbTimeoutConnect),
|
||||
];
|
||||
$out = [];
|
||||
if ($service != "") {
|
||||
|
|
|
@ -28,8 +28,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
// if no database file is specified in the configuration, use a suitable default
|
||||
$dbFile = $dbFile ?? Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db";
|
||||
$dbKey = $dbKey ?? Arsse::$conf->dbSQLite3Key;
|
||||
$dbFile = Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db";
|
||||
$dbKey = Arsse::$conf->dbSQLite3Key;
|
||||
$timeout = Arsse::$conf->dbSQLite3Timeout * 1000;
|
||||
try {
|
||||
$this->makeConnection($dbFile, $dbKey);
|
||||
|
@ -55,7 +55,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
throw new Exception("fileCorrupt", $dbFile);
|
||||
}
|
||||
// set the timeout
|
||||
$timeout = (int) ceil((Arsse::$conf->dbSQLite3Timeout ?? 0) * 1000);
|
||||
$timeout = (int) ceil(Arsse::$conf->dbSQLite3Timeout * 1000);
|
||||
$this->setTimeout($timeout);
|
||||
// set other initial options
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
|
|
12
lib/Lang.php
12
lib/Lang.php
|
@ -26,6 +26,8 @@ class Lang {
|
|||
protected $locale = ""; // the currently loaded locale
|
||||
protected $loaded = []; // the cascade of loaded locale file names
|
||||
protected $strings = self::REQUIRED; // the loaded locale strings, merged
|
||||
/** @var \MessageFormatter */
|
||||
protected $formatter;
|
||||
|
||||
public function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
|
||||
$this->path = $path;
|
||||
|
@ -101,9 +103,13 @@ class Lang {
|
|||
} elseif (!is_array($vars)) {
|
||||
$vars = [$vars];
|
||||
}
|
||||
$msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
|
||||
$this->formatter = $this->formatter ?? new \MessageFormatter($this->locale, "Initial message");
|
||||
if (!$this->formatter->setPattern($msg)) {
|
||||
throw new Lang\Exception("stringInvalid", ['error' => $this->formatter->getErrorMessage(), 'msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
|
||||
}
|
||||
$msg = $this->formatter->format($vars);
|
||||
if ($msg===false) {
|
||||
throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
|
||||
throw new Lang\Exception("dataInvalid", ['error' => $this->formatter->getErrorMessage(), 'msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]); // @codeCoverageIgnore
|
||||
}
|
||||
return $msg;
|
||||
}
|
||||
|
@ -159,6 +165,7 @@ class Lang {
|
|||
$this->strings = self::REQUIRED;
|
||||
$this->locale = $this->wanted;
|
||||
$this->synched = true;
|
||||
$this->formatter = null;
|
||||
return true;
|
||||
}
|
||||
// decompose the requested locale from specific to general, building a list of files to load
|
||||
|
@ -217,6 +224,7 @@ class Lang {
|
|||
$this->loaded = $loaded;
|
||||
$this->locale = $this->wanted;
|
||||
$this->synched = true;
|
||||
$this->formatter = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,16 +25,17 @@ class Date {
|
|||
return ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
|
||||
}
|
||||
|
||||
public static function add(string $interval, $date = "now") {
|
||||
public static function add($interval, $date = "now") {
|
||||
return self::modify("add", $interval, $date);
|
||||
}
|
||||
|
||||
public static function sub(string $interval, $date = "now") {
|
||||
public static function sub($interval, $date = "now") {
|
||||
return self::modify("sub", $interval, $date);
|
||||
}
|
||||
|
||||
protected static function modify(string $func, string $interval, $date) {
|
||||
protected static function modify(string $func, $interval, $date) {
|
||||
$date = self::normalize($date);
|
||||
return $date ? $date->$func(new \DateInterval($interval)) : null;
|
||||
$interval = (!$interval instanceof \DateInterval) ? ValueInfo::normalize($interval, ValueInfo::T_INTERVAL) : $interval;
|
||||
return $date ? $date->$func($interval) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class ValueInfo {
|
|||
const T_ARRAY = 7; // convert to array
|
||||
const T_INTERVAL = 8; // convert to time interval
|
||||
// normalization modes
|
||||
const M_LOOSE = 0;
|
||||
const M_NULL = 1 << 28; // pass nulls through regardless of target type
|
||||
const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match
|
||||
const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
|
||||
|
|
|
@ -146,7 +146,7 @@ class REST {
|
|||
}
|
||||
|
||||
public function challenge(ResponseInterface $res, string $realm = null): ResponseInterface {
|
||||
$realm = $realm ?? Arsse::$conf->httpRealm ?? "Default";
|
||||
$realm = $realm ?? Arsse::$conf->httpRealm;
|
||||
return $res->withAddedHeader("WWW-Authenticate", 'Basic realm="'.$realm.'"');
|
||||
}
|
||||
|
||||
|
@ -205,8 +205,8 @@ class REST {
|
|||
}
|
||||
|
||||
public function corsNegotiate(RequestInterface $req, string $allowed = null, string $denied = null): bool {
|
||||
$allowed = trim($allowed ?? Arsse::$conf->httpOriginsAllowed ?? "");
|
||||
$denied = trim($denied ?? Arsse::$conf->httpOriginsDenied ?? "");
|
||||
$allowed = trim($allowed ?? Arsse::$conf->httpOriginsAllowed);
|
||||
$denied = trim($denied ?? Arsse::$conf->httpOriginsDenied);
|
||||
// continue if at least one origin is allowed
|
||||
if ($allowed) {
|
||||
// continue if the request has exactly one Origin header
|
||||
|
|
|
@ -461,7 +461,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
}
|
||||
$children = $c['children'] ? $this->enumerateCategories($cats, $subs, $c['id'], $all) : ['list' => [], 'feeds' => 0];
|
||||
$feeds = $c['feeds'] ? $this->enumerateFeeds($subs, $c['id']) : [];
|
||||
$count = sizeof($feeds) + $children['feeds'];
|
||||
$count = sizeof($feeds) + (int) $children['feeds'];
|
||||
$out[] = [
|
||||
'name' => $c['name'],
|
||||
'id' => "CAT:".$c['id'],
|
||||
|
@ -472,7 +472,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'unread' => 0,
|
||||
'child_unread' => 0,
|
||||
'checkbox' => false,
|
||||
'param' => Arsse::$lang->msg("API.TTRSS.FeedCount", $count),
|
||||
'param' => Arsse::$lang->msg("API.TTRSS.FeedCount", (string) $count),
|
||||
'items' => array_merge($children['list'], $feeds),
|
||||
];
|
||||
$feedTotal += $count;
|
||||
|
|
|
@ -9,6 +9,11 @@ namespace JKingWeb\Arsse;
|
|||
use JKingWeb\Arsse\Misc\Date;
|
||||
|
||||
class Service {
|
||||
const DRIVER_NAMES = [
|
||||
'serial' => \JKingWeb\Arsse\Service\Serial\Driver::class,
|
||||
'subprocess' => \JKingWeb\Arsse\Service\Subprocess\Driver::class,
|
||||
'curl' => \JKingWeb\Arsse\Service\Curl\Driver::class,
|
||||
];
|
||||
|
||||
/** @var Service\Driver */
|
||||
protected $drv;
|
||||
|
@ -27,18 +32,10 @@ class Service {
|
|||
return $classes;
|
||||
}
|
||||
|
||||
public static function interval(): \DateInterval {
|
||||
try {
|
||||
return new \DateInterval(Arsse::$conf->serviceFrequency);
|
||||
} catch (\Exception $e) {
|
||||
return new \DateInterval("PT2M");
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
$driver = Arsse::$conf->serviceDriver;
|
||||
$this->drv = new $driver();
|
||||
$this->interval = static::interval();
|
||||
$this->interval = Arsse::$conf->serviceFrequency;
|
||||
}
|
||||
|
||||
public function watch(bool $loop = true): \DateTimeInterface {
|
||||
|
@ -77,8 +74,8 @@ class Service {
|
|||
// convert the check-in timestamp to a DateTime instance
|
||||
$checkin = Date::normalize($checkin, "sql");
|
||||
// get the checking interval
|
||||
$int = static::interval();
|
||||
// subtract twice the checking interval from the current time to the earliest acceptable check-in time
|
||||
$int = Arsse::$conf->serviceFrequency;
|
||||
// subtract twice the checking interval from the current time to yield the earliest acceptable check-in time
|
||||
$limit = new \DateTime();
|
||||
$limit->sub($int);
|
||||
$limit->sub($int);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Service\Internal;
|
||||
namespace JKingWeb\Arsse\Service\Serial;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
|
@ -12,7 +12,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
|||
protected $queue = [];
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Service.Internal.Name");
|
||||
return Arsse::$lang->msg("Driver.Service.Serial.Name");
|
||||
}
|
||||
|
||||
public static function requirementsMet(): bool {
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Service\Forking;
|
||||
namespace JKingWeb\Arsse\Service\Subprocess;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
|
@ -12,7 +12,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
|||
protected $queue = [];
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Service.Forking.Name");
|
||||
return Arsse::$lang->msg("Driver.Service.Subprocess.Name");
|
||||
}
|
||||
|
||||
public static function requirementsMet(): bool {
|
|
@ -9,6 +9,10 @@ namespace JKingWeb\Arsse;
|
|||
use PasswordGenerator\Generator as PassGen;
|
||||
|
||||
class User {
|
||||
const DRIVER_NAMES = [
|
||||
'internal' => \JKingWeb\Arsse\User\Internal\Driver::class,
|
||||
];
|
||||
|
||||
public $id = null;
|
||||
|
||||
/**
|
||||
|
|
100
locale/en.php
100
locale/en.php
|
@ -16,7 +16,7 @@ return [
|
|||
'API.TTRSS.Feed.Published' => 'Published articles',
|
||||
'API.TTRSS.Feed.Archived' => 'Archived articles',
|
||||
'API.TTRSS.Feed.Read' => 'Recently read',
|
||||
'API.TTRSS.FeedCount' => '{0, select, 1 {(1 feed)} other {({0} feeds)}}',
|
||||
'API.TTRSS.FeedCount' => '({0, number} {0, plural, one {feed} other {feeds}})',
|
||||
|
||||
'Driver.Db.SQLite3.Name' => 'SQLite 3',
|
||||
'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)',
|
||||
|
@ -24,74 +24,16 @@ return [
|
|||
'Driver.Db.PostgreSQLPDO.Name' => 'PostgreSQL (PDO)',
|
||||
'Driver.Db.MySQL.Name' => 'MySQL',
|
||||
'Driver.Db.MySQLPDO.Name' => 'MySQL (PDO)',
|
||||
'Driver.Service.Curl.Name' => 'HTTP (curl)',
|
||||
'Driver.Service.Internal.Name' => 'Internal',
|
||||
|
||||
'Driver.Service.Serial.Name' => 'Serialized',
|
||||
'Driver.Service.Subprocess.Name' => 'Concurrent subprocess',
|
||||
'Driver.Service.Curl.Name' => 'Concurrent HTTP (curl)',
|
||||
|
||||
'Driver.User.Internal.Name' => 'Internal',
|
||||
|
||||
'HTTP.Status.100' => 'Continue',
|
||||
'HTTP.Status.101' => 'Switching Protocols',
|
||||
'HTTP.Status.102' => 'Processing',
|
||||
'HTTP.Status.200' => 'OK',
|
||||
'HTTP.Status.201' => 'Created',
|
||||
'HTTP.Status.202' => 'Accepted',
|
||||
'HTTP.Status.203' => 'Non-Authoritative Information',
|
||||
'HTTP.Status.204' => 'No Content',
|
||||
'HTTP.Status.205' => 'Reset Content',
|
||||
'HTTP.Status.206' => 'Partial Content',
|
||||
'HTTP.Status.207' => 'Multi-Status',
|
||||
'HTTP.Status.208' => 'Already Reported',
|
||||
'HTTP.Status.226' => 'IM Used',
|
||||
'HTTP.Status.300' => 'Multiple Choice',
|
||||
'HTTP.Status.301' => 'Moved Permanently',
|
||||
'HTTP.Status.302' => 'Found',
|
||||
'HTTP.Status.303' => 'See Other',
|
||||
'HTTP.Status.304' => 'Not Modified',
|
||||
'HTTP.Status.305' => 'Use Proxy',
|
||||
'HTTP.Status.306' => 'Switch Proxy',
|
||||
'HTTP.Status.307' => 'Temporary Redirect',
|
||||
'HTTP.Status.308' => 'Permanent Redirect',
|
||||
'HTTP.Status.400' => 'Bad Request',
|
||||
'HTTP.Status.401' => 'Unauthorized',
|
||||
'HTTP.Status.402' => 'Payment Required',
|
||||
'HTTP.Status.403' => 'Forbidden',
|
||||
'HTTP.Status.404' => 'Not Found',
|
||||
'HTTP.Status.405' => 'Method Not Allowed',
|
||||
'HTTP.Status.406' => 'Not Acceptable',
|
||||
'HTTP.Status.407' => 'Proxy Authentication Required',
|
||||
'HTTP.Status.408' => 'Request Timeout',
|
||||
'HTTP.Status.409' => 'Conflict',
|
||||
'HTTP.Status.410' => 'Gone',
|
||||
'HTTP.Status.411' => 'Length Required',
|
||||
'HTTP.Status.412' => 'Precondition Failed',
|
||||
'HTTP.Status.413' => 'Payload Too Large',
|
||||
'HTTP.Status.414' => 'URL Too Long',
|
||||
'HTTP.Status.415' => 'Unsupported Media Type',
|
||||
'HTTP.Status.416' => 'Range Not Satisfiable',
|
||||
'HTTP.Status.417' => 'Expectation Failed',
|
||||
'HTTP.Status.421' => 'Misdirected Request',
|
||||
'HTTP.Status.422' => 'Unprocessable Entity',
|
||||
'HTTP.Status.423' => 'Locked',
|
||||
'HTTP.Status.424' => 'Failed Depedency',
|
||||
'HTTP.Status.426' => 'Upgrade Required',
|
||||
'HTTP.Status.428' => 'Precondition Failed',
|
||||
'HTTP.Status.429' => 'Too Many Requests',
|
||||
'HTTP.Status.431' => 'Request Header Fields Too Large',
|
||||
'HTTP.Status.451' => 'Unavailable For Legal Reasons',
|
||||
'HTTP.Status.500' => 'Internal Server Error',
|
||||
'HTTP.Status.501' => 'Not Implemented',
|
||||
'HTTP.Status.502' => 'Bad Gateway',
|
||||
'HTTP.Status.503' => 'Service Unavailable',
|
||||
'HTTP.Status.504' => 'Gateway Timeout',
|
||||
'HTTP.Status.505' => 'HTTP Version Not Supported',
|
||||
'HTTP.Status.506' => 'Variant Also Negotiates',
|
||||
'HTTP.Status.507' => 'Insufficient Storage',
|
||||
'HTTP.Status.508' => 'Loop Detected',
|
||||
'HTTP.Status.510' => 'Not Extended',
|
||||
'HTTP.Status.511' => 'Network Authentication Required',
|
||||
|
||||
// this should only be encountered in testing (because tests should cover all exceptions!)
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php',
|
||||
// this should not usually be encountered
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Exception.constantUnknown' => 'Supplied constant value ({0}) is unknown or invalid in the context in which it was used',
|
||||
|
@ -103,20 +45,36 @@ return [
|
|||
5 {datetime}
|
||||
6 {string}
|
||||
7 {array}
|
||||
other {requested type}
|
||||
8 {DateInterval}
|
||||
other {requested type {0}}
|
||||
}',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/ExceptionType.typeUnknown' => 'Normalization type {0} is not implemented',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList}): {error}',
|
||||
'Exception.JKingWeb/Arsse/Lang/Exception.dataInvalid' => 'Failed to format message message string "{msgID}" (language files loaded: {fileList}): {error}',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.typeMismatch' =>
|
||||
'Configuration parameter "{param}" in file "{file}" must be {type, select,
|
||||
integer {an integral number}
|
||||
string {a character string}
|
||||
boolean {either true or false}
|
||||
float {a decimal number}
|
||||
interval {an ISO 8601 time interval}
|
||||
other {consistent with type "{type}"}
|
||||
}{nullable, select,
|
||||
0 {}
|
||||
other {, or null}
|
||||
}',
|
||||
'Exception.JKingWeb/Arsse/Conf/Exception.semanticMismatch' => 'Configuration parameter "{param}" in file "{file}" is not a valid value. Consult the documentation for possible values',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
|
||||
|
@ -125,7 +83,9 @@ return [
|
|||
'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.connectionFailure' => 'Could not connect to {engine} database: {message}',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.updateManual' =>
|
||||
|
@ -149,9 +109,13 @@ return [
|
|||
other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
|
||||
}',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.savepointStatusUnknown' => 'Savepoint status code {0} not implemented',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.savepointInvalid' => 'Tried to {action} invalid savepoint {index}',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.savepointStale' => 'Tried to {action} stale savepoint {index}',
|
||||
// indicates programming error
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.resultReused' => 'Result set already iterated',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace',
|
||||
|
|
|
@ -44,7 +44,10 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @depends testLoadDefaultValues */
|
||||
public function testImportFromArray() {
|
||||
$arr = ['lang' => "xx"];
|
||||
$arr = [
|
||||
'lang' => "xx",
|
||||
'purgeFeeds' => "P2D",
|
||||
];
|
||||
$conf = new Conf;
|
||||
$conf->import($arr);
|
||||
$this->assertEquals("xx", $conf->lang);
|
||||
|
@ -96,6 +99,24 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$conf = new Conf(self::$path."confCorrupt");
|
||||
}
|
||||
|
||||
public function testImportBogusValue() {
|
||||
$arr = [
|
||||
'dbAutoUpdate' => "yes, please",
|
||||
];
|
||||
$conf = new Conf;
|
||||
$this->assertException("typeMismatch", "Conf");
|
||||
$conf->import($arr);
|
||||
}
|
||||
|
||||
public function testImportBogusDriver() {
|
||||
$arr = [
|
||||
'dbDriver' => "this driver does not exist",
|
||||
];
|
||||
$conf = new Conf;
|
||||
$this->assertException("semanticMismatch", "Conf");
|
||||
$conf->import($arr);
|
||||
}
|
||||
|
||||
public function testExportToArray() {
|
||||
$conf = new Conf;
|
||||
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
||||
|
|
|
@ -151,6 +151,20 @@ trait SeriesCleanup {
|
|||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
public function testCleanUpOrphanedFeedsWithUnlimitedRetention() {
|
||||
Arsse::$conf->import([
|
||||
'purgeFeeds' => null,
|
||||
]);
|
||||
Arsse::$db->feedCleanup();
|
||||
$now = gmdate("Y-m-d H:i:s");
|
||||
$state = $this->primeExpectations($this->data, [
|
||||
'arsse_feeds' => ["id","orphaned"]
|
||||
]);
|
||||
$state['arsse_feeds']['rows'][0][1] = null;
|
||||
$state['arsse_feeds']['rows'][2][1] = $now;
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
public function testCleanUpOldArticlesWithStandardRetention() {
|
||||
Arsse::$db->articleCleanup();
|
||||
$state = $this->primeExpectations($this->data, [
|
||||
|
@ -163,7 +177,9 @@ trait SeriesCleanup {
|
|||
}
|
||||
|
||||
public function testCleanUpOldArticlesWithUnlimitedReadRetention() {
|
||||
Arsse::$conf->purgeArticlesRead = "";
|
||||
Arsse::$conf->import([
|
||||
'purgeArticlesRead' => null,
|
||||
]);
|
||||
Arsse::$db->articleCleanup();
|
||||
$state = $this->primeExpectations($this->data, [
|
||||
'arsse_articles' => ["id"]
|
||||
|
@ -175,7 +191,9 @@ trait SeriesCleanup {
|
|||
}
|
||||
|
||||
public function testCleanUpOldArticlesWithUnlimitedUnreadRetention() {
|
||||
Arsse::$conf->purgeArticlesUnread = "";
|
||||
Arsse::$conf->import([
|
||||
'purgeArticlesUnread' => null,
|
||||
]);
|
||||
Arsse::$db->articleCleanup();
|
||||
$state = $this->primeExpectations($this->data, [
|
||||
'arsse_articles' => ["id"]
|
||||
|
@ -187,8 +205,10 @@ trait SeriesCleanup {
|
|||
}
|
||||
|
||||
public function testCleanUpOldArticlesWithUnlimitedRetention() {
|
||||
Arsse::$conf->purgeArticlesRead = "";
|
||||
Arsse::$conf->purgeArticlesUnread = "";
|
||||
Arsse::$conf->import([
|
||||
'purgeArticlesRead' => null,
|
||||
'purgeArticlesUnread' => null,
|
||||
]);
|
||||
Arsse::$db->articleCleanup();
|
||||
$state = $this->primeExpectations($this->data, [
|
||||
'arsse_articles' => ["id"]
|
||||
|
|
|
@ -896,7 +896,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
|
||||
public function testQueryTheServerStatus() {
|
||||
$interval = Service::interval();
|
||||
$interval = Arsse::$conf->serviceFrequency;
|
||||
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
|
||||
$invalid = $valid->sub($interval)->sub($interval);
|
||||
Phake::when(Arsse::$db)->metaGet("service_last_checkin")->thenReturn(Date::transform($valid, "sql"))->thenReturn(Date::transform($invalid, "sql"));
|
||||
|
|
|
@ -968,7 +968,7 @@ LONG_STRING;
|
|||
|
||||
public function testRetrieveTheServerConfiguration() {
|
||||
$in = ['op' => "getConfig", 'sid' => "PriestsOfSyrinx"];
|
||||
$interval = Service::interval();
|
||||
$interval = Arsse::$conf->serviceFrequency;
|
||||
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
|
||||
$invalid = $valid->sub($interval)->sub($interval);
|
||||
Phake::when(Arsse::$db)->metaGet("service_last_checkin")->thenReturn(Date::transform($valid, "sql"))->thenReturn(Date::transform($invalid, "sql"));
|
||||
|
|
|
@ -24,26 +24,6 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$this->srv = new Service();
|
||||
}
|
||||
|
||||
public function testComputeInterval() {
|
||||
$in = [
|
||||
Arsse::$conf->serviceFrequency,
|
||||
"PT2M",
|
||||
"PT5M",
|
||||
"P2M",
|
||||
"5M",
|
||||
"interval",
|
||||
];
|
||||
foreach ($in as $index => $spec) {
|
||||
try {
|
||||
$exp = new \DateInterval($spec);
|
||||
} catch (\Exception $e) {
|
||||
$exp = new \DateInterval("PT2M");
|
||||
}
|
||||
Arsse::$conf->serviceFrequency = $spec;
|
||||
$this->assertEquals($exp, Service::interval(), "Interval #$index '$spec' was not correctly calculated");
|
||||
}
|
||||
}
|
||||
|
||||
public function testCheckIn() {
|
||||
$now = time();
|
||||
$this->srv->checkIn();
|
||||
|
@ -54,7 +34,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
public function testReportHavingCheckedIn() {
|
||||
// the mock's metaGet() returns null by default
|
||||
$this->assertFalse(Service::hasCheckedIn());
|
||||
$interval = Service::interval();
|
||||
$interval = Arsse::$conf->serviceFrequency;
|
||||
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
|
||||
$invalid = $valid->sub($interval)->sub($interval);
|
||||
Phake::when(Arsse::$db)->metaGet("service_last_checkin")->thenReturn(Date::transform($valid, "sql"))->thenReturn(Date::transform($invalid, "sql"));
|
||||
|
|
|
@ -52,7 +52,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
'dbMySQLPass' => "arsse_test",
|
||||
'dbMySQLDb' => "arsse_test",
|
||||
];
|
||||
Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf);
|
||||
Arsse::$conf = (($force ? null : Arsse::$conf) ?? (new Conf))->import($defaults)->import($conf);
|
||||
}
|
||||
|
||||
public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") {
|
||||
|
@ -68,7 +68,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
$this->expectExceptionCode($code);
|
||||
} else {
|
||||
// expecting a standard PHP exception
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectException(\Throwable::class);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue