diff --git a/arsse.php b/arsse.php index 9a8d25d4..96e68446 100644 --- a/arsse.php +++ b/arsse.php @@ -1,10 +1,19 @@ watch(); + // initialize the CLI; this automatically handles --help and --version + $cli = new CLI; + // load configuration + Arsse::load(new Conf()); + // handle CLI requests + $cli->dispatch(); } else { - (new REST)->dispatch(); + // load configuration + Arsse::load(new Conf()); + // handle Web requests + (new REST)->dispatch()->output(); } \ No newline at end of file diff --git a/composer.json b/composer.json index 36f69d2a..776ba959 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "fguillot/picofeed": ">=0.1.31", "jkingweb/druuid": "^3.0.0", "phpseclib/phpseclib": "^2.0", - "hosteurope/password-generator": "^1.0" + "hosteurope/password-generator": "^1.0", + "docopt/docopt": "^1.0" }, "require-dev": { "mikey179/vfsStream": "^1.6", diff --git a/composer.lock b/composer.lock index 8f81bafe..8553bd56 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,54 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "f86e3cf99b80693dffb2a1e47e0b657d", + "content-hash": "360a767ae23dbd1b702c1b3b8b08b683", "packages": [ + { + "name": "docopt/docopt", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/docopt/docopt.php.git", + "reference": "d2ee65c2fe4be78f945a48edd02be45843b39423" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/docopt/docopt.php/zipball/d2ee65c2fe4be78f945a48edd02be45843b39423", + "reference": "d2ee65c2fe4be78f945a48edd02be45843b39423", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.1.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/docopt.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Blake Williams", + "email": "code@shabbyrobe.org", + "homepage": "http://docopt.org/", + "role": "Developer" + } + ], + "description": "Port of Python's docopt for PHP 5.3", + "homepage": "http://github.com/docopt/docopt.php", + "keywords": [ + "cli", + "docs" + ], + "time": "2015-10-30T03:21:23+00:00" + }, { "name": "fguillot/picofeed", "version": "v0.1.35", @@ -3000,7 +3046,7 @@ }, { "name": "symfony/config", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -3056,7 +3102,7 @@ }, { "name": "symfony/console", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -3174,7 +3220,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3283,7 +3329,7 @@ }, { "name": "symfony/finder", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3391,7 +3437,7 @@ }, { "name": "symfony/process", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -3440,7 +3486,7 @@ }, { "name": "symfony/stopwatch", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -3553,7 +3599,7 @@ }, { "name": "symfony/validator", - "version": "v2.8.24", + "version": "v2.8.25", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", diff --git a/lib/CLI.php b/lib/CLI.php new file mode 100644 index 00000000..506faefc --- /dev/null +++ b/lib/CLI.php @@ -0,0 +1,53 @@ + + $prog --version + $prog --help | -h + +The Arsse command-line interface currently allows you to start the refresh +daemon or refresh a specific feed by numeric ID. +USAGE_TEXT; + } + + function __construct(array $argv = null) { + if(is_null($argv)) { + $argv = array_slice($_SERVER['argv'], 1); + } + $this->args = \Docopt::handle($this->usage(), [ + 'argv' => $argv, + 'help' => true, + 'version' => VERSION, + ]); + } + + function dispatch(array $args = null): int { + // act on command line + if(is_null($args)) { + $args = $this->args; + } + if($args['daemon']) { + return $this->daemon(); + } elseif($args['feed'] && $args['refresh']) { + return $this->feedRefresh((int) $args['']); + } + } + + protected function daemon(bool $loop = true): int { + (new Service)->watch($loop); + return 0; // FIXME: should return the exception code of thrown exceptions + } + + protected function feedRefresh(int $id): int { + return (int) !Arsse::$db->feedUpdate($id); + } +} \ No newline at end of file diff --git a/lib/Conf.php b/lib/Conf.php index d4166f3d..37ab525c 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -56,7 +56,7 @@ class Conf { public $userTempPasswordLength = 20; /** @var string Class of the background feed update service driver in use (Forking by default) */ - public $serviceDriver = Service\Internal\Driver::class; + public $serviceDriver = Service\Forking\Driver::class; /** @var string The interval between checks for new feeds, as an ISO 8601 duration * @see https://en.wikipedia.org/wiki/ISO_8601#Durations */ @@ -84,7 +84,9 @@ class Conf { * @see self::importFile() */ public function __construct(string $import_file = "") { - if($import_file != "") $this->importFile($import_file); + if($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 @@ -100,8 +102,11 @@ class Conf { * 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. * @param string $file Full path and file name for the file to import */ public function importFile(string $file): self { - if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file); - if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file); + if(!file_exists($file)) { + throw new Conf\Exception("fileMissing", $file); + } else if(!is_readable($file)) { + throw new Conf\Exception("fileUnreadable", $file); + } try { ob_start(); $arr = (@include $file); @@ -110,7 +115,9 @@ class Conf { } finally { ob_end_clean(); } - if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file); + if(!is_array($arr)) { + throw new Conf\Exception("fileCorrupt", $file); + } return $this->import($arr); } diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index dc483b22..c04124d0 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -18,31 +18,34 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function __construct(bool $install = false) { // check to make sure required extension is loaded - if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName()); + if(!class_exists("SQLite3")) { + throw new Exception("extMissing", self::driverName()); + } $file = Arsse::$conf->dbSQLite3File; // if the file exists (or we're initializing the database), try to open it try { - $this->db = $this->makeConnection($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, Arsse::$conf->dbSQLite3Key); - } catch(\Throwable $e) { - // if opening the database doesn't work, check various pre-conditions to find out what the problem might be - if(!file_exists($file)) { - if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); - throw new Exception("fileMissing", $file); - } - if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file); - if(!is_readable($file)) throw new Exception("fileUnreadable", $file); - if(!is_writable($file)) throw new Exception("fileUnwritable", $file); - // otherwise the database is probably corrupt - throw new Exception("fileCorrupt", $mainfile); - } - try { + $this->db = $this->makeConnection($file, \SQLITE3_OPEN_CREATE | \SQLITE3_OPEN_READWRITE, Arsse::$conf->dbSQLite3Key); // set initial options $this->db->enableExceptions(true); $this->exec("PRAGMA journal_mode = wal"); $this->exec("PRAGMA foreign_keys = yes"); - } catch(\Exception $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); - throw new $excClass($excMsg, $excData); + } catch(\Throwable $e) { + // if opening the database doesn't work, check various pre-conditions to find out what the problem might be + if(!file_exists($file)) { + if($install && !is_writable(dirname($file))) { + throw new Exception("fileUncreatable", dirname($file)); + } + throw new Exception("fileMissing", $file); + } + if(!is_readable($file) && !is_writable($file)) { + throw new Exception("fileUnusable", $file); + } else if(!is_readable($file)) { + throw new Exception("fileUnreadable", $file); + } else if(!is_writable($file)) { + throw new Exception("fileUnwritable", $file); + } + // otherwise the database is probably corrupt + throw new Exception("fileCorrupt", $mainfile); } } @@ -66,8 +69,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function schemaUpdate(int $to): bool { $ver = $this->schemaVersion(); - if(!Arsse::$conf->dbAutoUpdate) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]); - if($ver >= $to) throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); + if(!Arsse::$conf->dbAutoUpdate) { + throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]); + } else if($ver >= $to) { + throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); + } $sep = \DIRECTORY_SEPARATOR; $path = Arsse::$conf->dbSchemaBase.$sep."SQLite3".$sep; // lock the database @@ -76,16 +82,23 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $this->savepointCreate(); try { $file = $path.$a.".sql"; - if(!file_exists($file)) throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); - if(!is_readable($file)) throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + if(!file_exists($file)) { + throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } else if(!is_readable($file)) { + throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } $sql = @file_get_contents($file); - if($sql===false) throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + if($sql===false) { + throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } try { $this->exec($sql); } catch(\Throwable $e) { throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]); } - if($this->schemaVersion() != $a+1) throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + if($this->schemaVersion() != $a+1) { + throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } } catch(\Throwable $e) { // undo any partial changes from the failed update $this->savepointUndo(); diff --git a/lib/REST.php b/lib/REST.php index 764f9e23..980a1bb5 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -30,7 +30,7 @@ class REST { function __construct() { } - function dispatch(REST\Request $req = null): bool { + function dispatch(REST\Request $req = null): REST\Response { if($req===null) { $req = new REST\Request(); } @@ -39,24 +39,7 @@ class REST { $req->refreshURL(); $class = $this->apis[$api]['class']; $drv = new $class(); - $out = $drv->dispatch($req); - header("Status: ".$out->code." ".Arsse::$lang->msg("HTTP.Status.".$out->code)); - if(!is_null($out->payload)) { - header("Content-Type: ".$out->type); - switch($out->type) { - case REST\Response::T_JSON: - $body = json_encode($out->payload,\JSON_PRETTY_PRINT); - break; - default: - $body = (string) $out->payload; - break; - } - } - foreach($out->fields as $field) { - header($field); - } - echo $body; - return true; + return $drv->dispatch($req); } function apiMatch(string $url, array $map): string { diff --git a/lib/REST/Exception.php b/lib/REST/Exception.php new file mode 100644 index 00000000..308621ef --- /dev/null +++ b/lib/REST/Exception.php @@ -0,0 +1,6 @@ +type = $type; $this->fields = $extraFields; } + + function output() { + if(!headers_sent()) { + header("Status: ".$this->code." ".Arsse::$lang->msg("HTTP.Status.".$this->code)); + $body = ""; + if(!is_null($this->payload)) { + header("Content-Type: ".$this->type); + switch($this->type) { + case self::T_JSON: + $body = json_encode($this->payload,\JSON_PRETTY_PRINT); + break; + default: + $body = (string) $this->payload; + break; + } + } + foreach($this->fields as $field) { + header($field); + } + echo $body; + } else { + throw new REST\Exception("headersSent"); + } + } } \ No newline at end of file diff --git a/lib/Service/Forking/Driver.php b/lib/Service/Forking/Driver.php new file mode 100644 index 00000000..b4d1e91e --- /dev/null +++ b/lib/Service/Forking/Driver.php @@ -0,0 +1,43 @@ +msg("Driver.Service.Forking.Name"); + } + + static function requirementsMet(): bool { + return function_exists("popen"); + } + + function __construct() { + } + + function queue(int ...$feeds): int { + $this->queue = array_merge($this->queue, $feeds); + return sizeof($this->queue); + } + + function exec(): int { + $pp = []; + while($this->queue) { + $id = (int) array_shift($this->queue); + array_push($pp, popen('"'.\PHP_BINARY.'" "'.$_SERVER['argv'][0].'" feed refresh '.$id, "r")); + } + while($pp) { + $p = array_pop($pp); + fgets($p); // TODO: log output + pclose($p); + } + return Arsse::$conf->serviceQueueWidth - sizeof($this->queue); + } + + function clean(): bool { + $this->queue = []; + return true; + } +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 4a915a91..f26b1dd8 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -15,6 +15,9 @@ ../lib + + + @@ -62,6 +65,5 @@ Service/TestService.php - \ No newline at end of file