Implemented configuration exporting; fixes #63
Default user agent string creation moved to Feed class as a consequence of difficulties in exporting it reliably
This commit is contained in:
parent
5df7217cff
commit
78faf88563
3 changed files with 120 additions and 37 deletions
77
lib/Conf.php
77
lib/Conf.php
|
@ -6,8 +6,7 @@ namespace JKingWeb\Arsse;
|
||||||
/** Class for loading, saving, and querying configuration
|
/** 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.
|
* 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.
|
* All public properties are configuration parameters that may be set by the server administrator. */
|
||||||
*/
|
|
||||||
class Conf {
|
class Conf {
|
||||||
/** @var string Default language to use for logging and errors */
|
/** @var string Default language to use for logging and errors */
|
||||||
public $lang = "en";
|
public $lang = "en";
|
||||||
|
@ -58,8 +57,7 @@ class Conf {
|
||||||
/** @var string Class of the background feed update service driver in use (Forking by default) */
|
/** @var string Class of the background feed update service driver in use (Forking by default) */
|
||||||
public $serviceDriver = Service\Forking\Driver::class;
|
public $serviceDriver = Service\Forking\Driver::class;
|
||||||
/** @var string The interval between checks for new feeds, as an ISO 8601 duration
|
/** @var string The interval between checks for new feeds, as an ISO 8601 duration
|
||||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations
|
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||||
*/
|
|
||||||
public $serviceFrequency = "PT2M";
|
public $serviceFrequency = "PT2M";
|
||||||
/** @var integer Number of concurrent feed updates to perform */
|
/** @var integer Number of concurrent feed updates to perform */
|
||||||
public $serviceQueueWidth = 5;
|
public $serviceQueueWidth = 5;
|
||||||
|
@ -81,20 +79,11 @@ class Conf {
|
||||||
|
|
||||||
/** Creates a new configuration object
|
/** Creates a new configuration object
|
||||||
* @param string $import_file Optional file to read configuration data from
|
* @param string $import_file Optional file to read configuration data from
|
||||||
* @see self::importFile()
|
* @see self::importFile() */
|
||||||
*/
|
|
||||||
public function __construct(string $import_file = "") {
|
public function __construct(string $import_file = "") {
|
||||||
if($import_file != "") {
|
if($import_file != "") {
|
||||||
$this->importFile($import_file);
|
$this->importFile($import_file);
|
||||||
}
|
}
|
||||||
if(is_null($this->fetchUserAgentString)) {
|
|
||||||
$this->fetchUserAgentString = sprintf('Arsse/%s (%s %s; %s; https://code.jkingweb.ca/jking/arsse) PicoFeed (https://github.com/fguillot/picoFeed)',
|
|
||||||
VERSION, // Arsse version
|
|
||||||
php_uname('s'), // OS
|
|
||||||
php_uname('r'), // OS version
|
|
||||||
php_uname('m') // platform architecture
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Layers configuration data from a file into an existing object
|
/** Layers configuration data from a file into an existing object
|
||||||
|
@ -132,17 +121,57 @@ class Conf {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Outputs non-default configuration settings as a string compatible with var_export()
|
/** 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 */
|
||||||
* If provided a file name, will produce the text of a PHP script suitable for later import
|
public function export(bool $full = false): array {
|
||||||
* @param string $file Full path and file name for the file to export to */
|
$ref = new self;
|
||||||
public function export(string $file = ""): string {
|
$out = [];
|
||||||
// TODO: write export method
|
$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 and either:
|
||||||
|
// 1. full output has been requested
|
||||||
|
// 2. the property is not defined in the class
|
||||||
|
// 3. it differs from the default
|
||||||
|
if(is_scalar($this->$name) && ($full || !$prop->isDefault() || $this->$name !== $ref->$name)) {
|
||||||
|
$out[$name] = $this->$name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias of export() method with no parameters
|
/** Outputs configuration settings, either non-default ones or all, to a file in a format suitable for later import
|
||||||
* @see self::export() */
|
* @param string $file Full path and file name for the file to import to; the containing directory must already exist
|
||||||
public function __toString(): string {
|
* @param bool $full Whether to output all configuration options rather than only changed ones */
|
||||||
return $this->export();
|
public function exportFile(string $file, bool $full = false): bool {
|
||||||
|
$arr = $this->export($full);
|
||||||
|
$conf = new \ReflectionObject($this);
|
||||||
|
$out = "<?php return [".PHP_EOL;
|
||||||
|
foreach($arr as $prop => $value) {
|
||||||
|
$match = null;
|
||||||
|
$doc = $comment = "";
|
||||||
|
// retrieve the property's docblock, if it exists
|
||||||
|
try {
|
||||||
|
$doc = (new \ReflectionProperty(self::class, $prop))->getDocComment();
|
||||||
|
} catch(\ReflectionException $e) {}
|
||||||
|
if($doc) {
|
||||||
|
// parse the docblock to extract the property description
|
||||||
|
if(preg_match("<@var\s+\S+\s+(.+?)(?:\s*\*/)?$>m", $doc, $match)) {
|
||||||
|
$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
|
||||||
|
$out .= " ".var_export($prop, true)." => ".var_export($value,true).",".PHP_EOL;
|
||||||
|
}
|
||||||
|
$out .= "];".PHP_EOL;
|
||||||
|
// write the configuration representation to the requested file
|
||||||
|
if(!@file_put_contents($file,$out)) {
|
||||||
|
// if it fails throw an exception
|
||||||
|
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
|
||||||
|
throw new Conf\Exception($err, $file);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
10
lib/Feed.php
10
lib/Feed.php
|
@ -22,12 +22,18 @@ class Feed {
|
||||||
|
|
||||||
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) {
|
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) {
|
||||||
// set the configuration
|
// set the configuration
|
||||||
|
$userAgent = Arsse::$conf->fetchUserAgentString ?? sprintf('Arsse/%s (%s %s; %s; https://code.jkingweb.ca/jking/arsse) PicoFeed (https://github.com/fguillot/picoFeed)',
|
||||||
|
VERSION, // Arsse version
|
||||||
|
php_uname('s'), // OS
|
||||||
|
php_uname('r'), // OS version
|
||||||
|
php_uname('m') // platform architecture
|
||||||
|
);
|
||||||
$this->config = new Config;
|
$this->config = new Config;
|
||||||
$this->config->setMaxBodySize(Arsse::$conf->fetchSizeLimit);
|
$this->config->setMaxBodySize(Arsse::$conf->fetchSizeLimit);
|
||||||
$this->config->setClientTimeout(Arsse::$conf->fetchTimeout);
|
$this->config->setClientTimeout(Arsse::$conf->fetchTimeout);
|
||||||
$this->config->setGrabberTimeout(Arsse::$conf->fetchTimeout);
|
$this->config->setGrabberTimeout(Arsse::$conf->fetchTimeout);
|
||||||
$this->config->setClientUserAgent(Arsse::$conf->fetchUserAgentString);
|
$this->config->setClientUserAgent($userAgent);
|
||||||
$this->config->setGrabberUserAgent(Arsse::$conf->fetchUserAgentString);
|
$this->config->setGrabberUserAgent($userAgent);
|
||||||
// fetch the feed
|
// fetch the feed
|
||||||
$this->download($url, $lastModified, $etag, $username, $password);
|
$this->download($url, $lastModified, $etag, $username, $password);
|
||||||
// format the HTTP Last-Modified date returned
|
// format the HTTP Last-Modified date returned
|
||||||
|
|
|
@ -17,10 +17,13 @@ class TestConf extends Test\AbstractTest {
|
||||||
'confNotPHP' => 'DEAD BEEF',
|
'confNotPHP' => 'DEAD BEEF',
|
||||||
'confEmpty' => '',
|
'confEmpty' => '',
|
||||||
'confUnreadable' => '',
|
'confUnreadable' => '',
|
||||||
|
'confForbidden' => [],
|
||||||
]);
|
]);
|
||||||
self::$path = self::$vfs->url()."/";
|
self::$path = self::$vfs->url()."/";
|
||||||
// set up a file without read access
|
// set up a file without read or write access
|
||||||
chmod(self::$path."confUnreadable", 0000);
|
chmod(self::$path."confUnreadable", 0000);
|
||||||
|
// set up a directory without read or write access
|
||||||
|
chmod(self::$path."confForbidden", 0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
function tearDown() {
|
||||||
|
@ -86,4 +89,49 @@ class TestConf extends Test\AbstractTest {
|
||||||
$this->assertException("fileCorrupt", "Conf");
|
$this->assertException("fileCorrupt", "Conf");
|
||||||
$conf = new Conf(self::$path."confCorrupt");
|
$conf = new Conf(self::$path."confCorrupt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testExportToArray() {
|
||||||
|
$conf = new Conf();
|
||||||
|
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
||||||
|
$conf->dbSchemaBase = "schema"; // should be exported: value changed
|
||||||
|
$conf->someCustomProperty = "Look at me!"; // should be exported: unknown property
|
||||||
|
$exp = [
|
||||||
|
'dbSchemaBase' => "schema",
|
||||||
|
'someCustomProperty' => "Look at me!",
|
||||||
|
];
|
||||||
|
$this->assertSame($exp, $conf->export());
|
||||||
|
$res = $conf->export(true); // export all properties
|
||||||
|
$this->assertNotSame($exp, $res);
|
||||||
|
$this->assertArraySubset($exp, $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @depends testExportToArray
|
||||||
|
* @depends testImportFromFile */
|
||||||
|
function testExportToFile() {
|
||||||
|
$conf = new Conf();
|
||||||
|
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
||||||
|
$conf->dbSchemaBase = "schema"; // should be exported: value changed
|
||||||
|
$conf->someCustomProperty = "Look at me!"; // should be exported: unknown property
|
||||||
|
$conf->exportFile(self::$path."confNotArray");
|
||||||
|
$arr = (include self::$path."confNotArray");
|
||||||
|
$exp = [
|
||||||
|
'dbSchemaBase' => "schema",
|
||||||
|
'someCustomProperty' => "Look at me!",
|
||||||
|
];
|
||||||
|
$this->assertSame($exp, $arr);
|
||||||
|
$conf->exportFile(self::$path."confNotArray", true); // export all properties
|
||||||
|
$arr = (include self::$path."confNotArray");
|
||||||
|
$this->assertNotSame($exp, $arr);
|
||||||
|
$this->assertArraySubset($exp, $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExportToFileWithoutWritePermission() {
|
||||||
|
$this->assertException("fileUnwritable", "Conf");
|
||||||
|
(new Conf)->exportFile(self::$path."confUnreadable");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExportToFileWithoutCreatePermission() {
|
||||||
|
$this->assertException("fileUncreatable", "Conf");
|
||||||
|
(new Conf)->exportFile(self::$path."confForbidden/conf");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue