diff --git a/lib/AbstractException.php b/lib/AbstractException.php index e4f22a9a..1e3a2dd3 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -86,8 +86,14 @@ abstract class AbstractException extends \Exception { "Feed/Exception.xmlEntity" => 10512, "Feed/Exception.subscriptionNotFound" => 10521, "Feed/Exception.unsupportedFeedFormat" => 10522, + "ImportExport/Exception.fileMissing" => 10601, + "ImportExport/Exception.fileUnreadable" => 10603, "ImportExport/Exception.fileUnwritable" => 10604, "ImportExport/Exception.fileUncreatable" => 10605, + "ImportExport/Exception.invalidSyntax" => 10611, + "ImportExport/Exception.invalidSemantics" => 10612, + "ImportExport/Exception.invalidFolderName" => 10613, + "ImportExport/Exception.invalidFolderCopy" => 10614, ]; public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { diff --git a/lib/CLI.php b/lib/CLI.php index 218e3d30..e8998962 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -24,7 +24,10 @@ Usage: arsse.php user unset-pass [--oldpass=] [--fever] arsse.php user auth [--fever] - arsse.php export [] [-f | --flat] + arsse.php export [] + [-f | --flat] + arsse.php import [] + [-f | --flat] [-r | --replace] arsse.php --version arsse.php --help | -h @@ -70,7 +73,7 @@ USAGE_TEXT; 'help' => false, ]); try { - $cmd = $this->command(["--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user", "export"], $args); + $cmd = $this->command(["--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user", "export", "import"], $args); if ($cmd && !in_array($cmd, ["--help", "--version", "conf save-defaults"])) { // only certain commands don't require configuration to be loaded $this->loadConf(); @@ -99,6 +102,10 @@ USAGE_TEXT; $u = $args['']; $file = $this->resolveFile($args[''], "w"); return (int) !$this->getInstance(OPML::class)->exportFile($file, $u, $args['--flat']); + case "import": + $u = $args['']; + $file = $this->resolveFile($args[''], "w"); + return (int) !$this->getInstance(OPML::class)->importFile($file, $u, $args['--flat'], $args['--replace']); } } catch (AbstractException $e) { $this->logError($e->getMessage()); diff --git a/lib/ImportExport/OPML.php b/lib/ImportExport/OPML.php index 616fa82d..be7365a7 100644 --- a/lib/ImportExport/OPML.php +++ b/lib/ImportExport/OPML.php @@ -19,14 +19,14 @@ class OPML { foreach ($folders as $f) { // check to make sure folder names are all valid if (!strlen(trim($f['name']))) { - throw new \Exception; + throw new Exception("invalidFolderName"); } // check for duplicates if (!isset($folderMap[$f['parent']])) { $folderMap[$f['parent']] = []; } if (isset($folderMap[$f['parent']][$f['name']])) { - throw new \Exception; + throw new Exception("invalidFolderCopy"); } else { $folderMap[$f['parent']][$f['name']] = true; } @@ -142,12 +142,13 @@ class OPML { $d = new \DOMDocument; if (!@$d->loadXML($opml)) { // not a valid XML document - throw new \Exception; + $err = libxml_get_last_error(); + throw new Exception("invalidSyntax", ['line' => $err->line, 'column' => $err->column]); } $body = $d->getElementsByTagName("body"); if ($d->documentElement->nodeName !== "opml" || !$body->length || $body->item(0)->parentNode != $d->documentElement) { // not a valid OPML document - throw new \Exception; + throw new Exception("invalidSemantics", ['type' => "OPML"]); } $body = $body->item(0); $folders = []; @@ -268,4 +269,14 @@ class OPML { } return true; } + + public function imortFile(string $file, string $user, bool $flat = false, bool $replace): bool { + $data = @file_get_contents($file); + if ($data === false) { + // if it fails throw an exception + $err = file_exists($file) ? "fileUnreadable" : "fileMissing"; + throw new Exception($err, ['file' => $file, 'format' => str_replace(__NAMESPACE__."\\", "", __CLASS__)]); + } + return $this->import($user, $data, $flat, $replace); + } } diff --git a/locale/en.php b/locale/en.php index a9fa045f..19fc7241 100644 --- a/locale/en.php +++ b/locale/en.php @@ -155,14 +155,12 @@ return [ 'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack', 'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"', 'Exception.JKingWeb/Arsse/Feed/Exception.unsupportedFeedFormat' => 'Feed "{url}" is of an unsupported format', - 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileUncreatable' => - 'Insufficient permissions to write {type, select, - OPML {OPML} - other {"{type}"} - } export to file "{file}"', - 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileUnwritable' => - 'Insufficient permissions to write {type, select, - OPML {OPML} - other {"{type}"} - } export to existing file "{file}"', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileMissing' => 'Import {type} file "{file}" does not exist', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileUnreadable' => 'Insufficient permissions to read {type} file "{file}" for import', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileUncreatable' => 'Insufficient permissions to write {type} export to file "{file}"', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.fileUnwritable' => 'Insufficient permissions to write {type} export to existing file "{file}"', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidSyntax' => 'Input data syntax error at line {line}, column {column}', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidSemantics' => 'Input data is not valid {type} data', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidFolderName' => 'Input data contains an invalid folder name', + 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidFolderCopy' => 'Input data contains multiple folders of the same name under the same parent', ];