diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 1bc47dde..2e6c23b1 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -42,6 +42,7 @@ Miniflux version 2.0.28 is emulated, though not all features are implemented - Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization - Querying articles for both read/unread and removed statuses will not return all removed articles - Search strings will match partial words +- OPML import either succeeds or fails atomically: if one feed fails, no feeds are imported # Behaviour of filtering (block and keep) rules diff --git a/lib/AbstractException.php b/lib/AbstractException.php index b6696c92..922b9cd8 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -104,7 +104,12 @@ abstract class AbstractException extends \Exception { "Rule/Exception.invalidPattern" => 10701, ]; + protected $symbol; + protected $params; + public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { + $this->symbol = $msgID; + $this->params = $vars ?? []; if ($msgID === "") { $msg = "Exception.unknown"; $code = 10000; @@ -121,4 +126,12 @@ abstract class AbstractException extends \Exception { } parent::__construct($msg, $code, $e); } + + public function getSymbol(): string { + return $this->symbol; + } + + public function getParams(): array { + return $this->aparams; + } } diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2198ebb6..00c58f02 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -13,7 +13,8 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\Misc\HTTP; +use JKingWeb\Arsse\ImportExport\OPML; +use JKingWeb\Arsse\ImportExport\Exception as ImportException; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\URL; use JKingWeb\Arsse\Misc\ValueInfo as V; @@ -25,6 +26,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse as Response; +use Laminas\Diactoros\Response\TextResponse as GenericResponse; use Laminas\Diactoros\Uri; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { @@ -141,8 +143,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'GET' => ["getCategoryFeeds", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ - 'PUT' => ["markCategory", false, true, false, false, []], ], + 'PUT' => ["markCategory", false, true, false, false, []], '/discover' => [ 'POST' => ["discoverSubscriptions", false, false, true, false, ["url"]], ], @@ -212,6 +214,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } + /** @codeCoverageIgnore */ + protected function getInstance(string $class) { + return new $class; + } + protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -261,9 +268,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } if ($reqBody) { if ($func === "opmlImport") { - if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { - return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); - } $args[] = (string) $req->getBody(); } else { $data = (string) $req->getBody(); @@ -1177,6 +1181,32 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function opmlImport(string $data): ResponseInterface { + try { + $this->getInstance(OPML::class)->import(Arsse::$user->id, $data); + } catch (ImportException $e) { + switch ($e->getCode()) { + case 10611: + return new ErrorResponse("InvalidBodyXML", 400); + case 10612: + return new ErrorResponse("InvalidBodyOPML", 422); + case 10613: + return new ErrorResponse("InvalidImportCategory", 422); + case 10614: + return new ErrorResponse("DuplicateImportCatgory", 422); + case 10615: + return new ErrorResponse("InvalidImportLabel", 422); + } + } catch (FeedException $e) { + return new ErrorResponse(["FailedImportFeed", 'url' => $e->getParams()['url'], 'code' => $e->getCode()], 502); + } + return new Response(['message' => Arsse::$lang->msg("ImportSuccess")]); + } + + protected function opmlExport(): ResponseInterface { + return new GenericResponse($this->getInstance(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokenss in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index 03b0579f..470d09ea 100644 --- a/locale/en.php +++ b/locale/en.php @@ -8,12 +8,15 @@ return [ 'CLI.Auth.Failure' => 'Authentication failed', 'API.Miniflux.DefaultCategoryName' => "All", + 'API.Miniflux.ImportSuccess' => 'Feeds imported successfully', 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', 'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input', 'API.Miniflux.Error.DuplicateInputValue' => 'Key "{field}" accepts only one value', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', + 'API.Miniflux.Error.InvalidBodyXML' => 'Invalid XML payload', + 'API.Miniflux.Error.InvalidBodyOPML' => 'Payload is not a valid OPML document', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', 'API.Miniflux.Error.Fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', @@ -28,6 +31,10 @@ return [ 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', 'API.Miniflux.Error.DuplicateFeed' => 'This feed already exists.', 'API.Miniflux.Error.InvalidTitle' => 'Invalid feed title', + 'API.Miniflux.Error.InvalidImportCategory' => 'Payload contains an invalid category name', + 'API.Miniflux.Error.DuplicateImportCategory' => 'Payload contains the same category name twice', + 'API.Miniflux.Error.FailedImportFeed' => 'Unable to import feed at URL "{url}" (code {code}', + 'API.Miniflux.Error.InvalidImportLabel' => 'Payload contains an invalid label name', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special',