mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2025-04-23 22:05:50 +00:00
More format-neutral code out of OPML class
This commit is contained in:
parent
92b1626dba
commit
7046ce163c
4 changed files with 186 additions and 166 deletions
167
lib/ImportExport/AbstractImportExport.php
Normal file
167
lib/ImportExport/AbstractImportExport.php
Normal file
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\ImportExport;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput as InputException;
|
||||
use JKingWeb\Arsse\User\Exception as UserException;
|
||||
|
||||
abstract class AbstractImportExport {
|
||||
public function import(string $user, string $data, bool $flat = false, bool $replace = false): bool {
|
||||
if (!Arsse::$user->exists($user)) {
|
||||
throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
// first extract useful information from the input
|
||||
list($feeds, $folders) = $this->parse($data, $flat);
|
||||
$folderMap = [];
|
||||
foreach ($folders as $f) {
|
||||
// check to make sure folder names are all valid
|
||||
if (!strlen(trim($f['name']))) {
|
||||
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("invalidFolderCopy");
|
||||
} else {
|
||||
$folderMap[$f['parent']][$f['name']] = true;
|
||||
}
|
||||
}
|
||||
// get feed IDs for each URL, adding feeds where necessary
|
||||
foreach ($feeds as $k => $f) {
|
||||
$feeds[$k]['id'] = Arsse::$db->feedAdd(($f['url']));
|
||||
}
|
||||
// start a transaction for atomic rollback
|
||||
$tr = Arsse::$db->begin();
|
||||
// get current state of database
|
||||
$foldersDb = iterator_to_array(Arsse::$db->folderList($user));
|
||||
$feedsDb = iterator_to_array(Arsse::$db->subscriptionList($user));
|
||||
$tagsDb = iterator_to_array(Arsse::$db->tagList($user));
|
||||
// reconcile folders
|
||||
$folderMap = [0 => 0];
|
||||
foreach ($folders as $id => $f) {
|
||||
$parent = $folderMap[$f['parent']];
|
||||
// find a match for the import folder in the existing folders
|
||||
foreach ($foldersDb as $db) {
|
||||
if ((int) $db['parent'] == $parent && $db['name'] === $f['name']) {
|
||||
$folderMap[$id] = (int) $db['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isset($folderMap[$id])) {
|
||||
// if no existing folder exists, add one
|
||||
$folderMap[$id] = Arsse::$db->folderAdd($user, ['name' => $f['name'], 'parent' -> $parent]);
|
||||
}
|
||||
}
|
||||
// process newsfeed subscriptions
|
||||
$feedMap = [];
|
||||
$tagMap = [];
|
||||
foreach ($feeds as $f) {
|
||||
$folder = $folderMap[$f['folder']];
|
||||
$title = strlen(trim($f['title'])) ? $f['title'] : null;
|
||||
$found = false;
|
||||
// find a match for the import feed is existing subscriptions
|
||||
foreach ($feedsDb as $db) {
|
||||
if ((int) $db['feed'] == $f['id']) {
|
||||
$found = true;
|
||||
$feedMap[$f['id']] = (int) $db['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
// if no subscription exists, add one
|
||||
$feedMap[$f['id']] = Arsse::$db->subscriptionAdd($user, $f['url']);
|
||||
}
|
||||
if (!$found || $replace) {
|
||||
// set the subscription's properties, if this is a new feed or we're doing a full replacement
|
||||
Arsse::$db->subscriptionPropertiesSet($user, $feedMap[$f['id']], ['title' => $title, 'folder' => $folder]);
|
||||
// compile the set of used tags, if this is a new feed or we're doing a full replacement
|
||||
foreach ($f['tags'] as $t) {
|
||||
if (!strlen(trim($t))) {
|
||||
// ignore any blank tags
|
||||
continue;
|
||||
}
|
||||
if (!isset($tagMap[$t])) {
|
||||
// populate the tag map
|
||||
$tagMap[$t] = [];
|
||||
}
|
||||
$tagMap[$t][] = $f['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// set tags
|
||||
$mode = $replace ? Database::ASSOC_REPLACE : Database::ASSOC_ADD;
|
||||
foreach ($tagMap as $tag => $subs) {
|
||||
// make sure the tag exists
|
||||
$found = false;
|
||||
foreach ($tagsDb as $db) {
|
||||
if ($tag === $db['name']) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
// add the tag if it wasn't found
|
||||
Arsse::$db->tagAdd($user, ['name' => $tag]);
|
||||
}
|
||||
Arsse::$db->tagSubscriptionsSet($user, $tag, $subs, $mode, true);
|
||||
}
|
||||
// finally, if we're performing a replacement, delete any subscriptions, folders, or tags which were not present in the import
|
||||
if ($replace) {
|
||||
foreach (array_diff(array_column($feedsDb, "id"), $feedMap) as $id) {
|
||||
try {
|
||||
Arsse::$db->subscriptionRemove($user, $id);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
foreach (array_diff(array_column($foldersDb, "id"), $folderMap) as $id) {
|
||||
try {
|
||||
Arsse::$db->folderRemove($user, $id);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
foreach (array_diff(array_column($tagsDb, "name"), array_keys($tagMap)) as $id) {
|
||||
try {
|
||||
Arsse::$db->tagRemove($user, $id, true);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
$tr->commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract public function parse(string $data, bool $flat): array;
|
||||
|
||||
abstract public function export(string $user, bool $flat = false): string;
|
||||
|
||||
public function exportFile(string $file, string $user, bool $flat = false): bool {
|
||||
$data = $this->export($user, $flat);
|
||||
if (!@file_put_contents($file, $data)) {
|
||||
// if it fails throw an exception
|
||||
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
|
||||
throw new Exception($err, ['file' => $file, 'format' => str_replace(__NAMESPACE__."\\", "", get_class($this))]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function importFile(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__."\\", "", get_class($this))]);
|
||||
}
|
||||
return $this->import($user, $data, $flat, $replace);
|
||||
}
|
||||
}
|
|
@ -7,137 +7,9 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse\ImportExport;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput as InputException;
|
||||
use JKingWeb\Arsse\User\Exception as UserException;
|
||||
|
||||
class OPML {
|
||||
public function import(string $user, string $opml, bool $flat = false, bool $replace = false): bool {
|
||||
// first extract useful information from the input
|
||||
list($feeds, $folders) = $this->parse($opml, $flat);
|
||||
$folderMap = [];
|
||||
foreach ($folders as $f) {
|
||||
// check to make sure folder names are all valid
|
||||
if (!strlen(trim($f['name']))) {
|
||||
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("invalidFolderCopy");
|
||||
} else {
|
||||
$folderMap[$f['parent']][$f['name']] = true;
|
||||
}
|
||||
}
|
||||
// get feed IDs for each URL, adding feeds where necessary
|
||||
foreach ($feeds as $k => $f) {
|
||||
$feeds[$k]['id'] = Arsse::$db->feedAdd(($f['url']));
|
||||
}
|
||||
// start a transaction for atomic rollback
|
||||
$tr = Arsse::$db->begin();
|
||||
// get current state of database
|
||||
$foldersDb = iterator_to_array(Arsse::$db->folderList($user));
|
||||
$feedsDb = iterator_to_array(Arsse::$db->subscriptionList($user));
|
||||
$tagsDb = iterator_to_array(Arsse::$db->tagList($user));
|
||||
// reconcile folders
|
||||
$folderMap = [0 => 0];
|
||||
foreach ($folders as $id => $f) {
|
||||
$parent = $folderMap[$f['parent']];
|
||||
// find a match for the import folder in the existing folders
|
||||
foreach ($foldersDb as $db) {
|
||||
if ((int) $db['parent'] == $parent && $db['name'] === $f['name']) {
|
||||
$folderMap[$id] = (int) $db['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isset($folderMap[$id])) {
|
||||
// if no existing folder exists, add one
|
||||
$folderMap[$id] = Arsse::$db->folderAdd($user, ['name' => $f['name'], 'parent' -> $parent]);
|
||||
}
|
||||
}
|
||||
// process newsfeed subscriptions
|
||||
$feedMap = [];
|
||||
$tagMap = [];
|
||||
foreach ($feeds as $f) {
|
||||
$folder = $folderMap[$f['folder']];
|
||||
$title = strlen(trim($f['title'])) ? $f['title'] : null;
|
||||
$found = false;
|
||||
// find a match for the import feed is existing subscriptions
|
||||
foreach ($feedsDb as $db) {
|
||||
if ((int) $db['feed'] == $f['id']) {
|
||||
$found = true;
|
||||
$feedMap[$f['id']] = (int) $db['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
// if no subscription exists, add one
|
||||
$feedMap[$f['id']] = Arsse::$db->subscriptionAdd($user, $f['url']);
|
||||
}
|
||||
if (!$found || $replace) {
|
||||
// set the subscription's properties, if this is a new feed or we're doing a full replacement
|
||||
Arsse::$db->subscriptionPropertiesSet($user, $feedMap[$f['id']], ['title' => $title, 'folder' => $folder]);
|
||||
// compile the set of used tags, if this is a new feed or we're doing a full replacement
|
||||
foreach ($f['tags'] as $t) {
|
||||
if (!strlen(trim($t))) {
|
||||
// ignore any blank tags
|
||||
continue;
|
||||
}
|
||||
if (!isset($tagMap[$t])) {
|
||||
// populate the tag map
|
||||
$tagMap[$t] = [];
|
||||
}
|
||||
$tagMap[$t][] = $f['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// set tags
|
||||
$mode = $replace ? Database::ASSOC_REPLACE : Database::ASSOC_ADD;
|
||||
foreach ($tagMap as $tag => $subs) {
|
||||
// make sure the tag exists
|
||||
$found = false;
|
||||
foreach ($tagsDb as $db) {
|
||||
if ($tag === $db['name']) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
// add the tag if it wasn't found
|
||||
Arsse::$db->tagAdd($user, ['name' => $tag]);
|
||||
}
|
||||
Arsse::$db->tagSubscriptionsSet($user, $tag, $subs, $mode, true);
|
||||
}
|
||||
// finally, if we're performing a replacement, delete any subscriptions, folders, or tags which were not present in the import
|
||||
if ($replace) {
|
||||
foreach (array_diff(array_column($feedsDb, "id"), $feedMap) as $id) {
|
||||
try {
|
||||
Arsse::$db->subscriptionRemove($user, $id);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
foreach (array_diff(array_column($foldersDb, "id"), $folderMap) as $id) {
|
||||
try {
|
||||
Arsse::$db->folderRemove($user, $id);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
foreach (array_diff(array_column($tagsDb, "name"), array_keys($tagMap)) as $id) {
|
||||
try {
|
||||
Arsse::$db->tagRemove($user, $id, true);
|
||||
} catch (InputException $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
$tr->commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
class OPML extends AbstractImportExport {
|
||||
public function parse(string $opml, bool $flat): array {
|
||||
$d = new \DOMDocument;
|
||||
if (!@$d->loadXML($opml)) {
|
||||
|
@ -276,24 +148,4 @@ class OPML {
|
|||
// return the serialization
|
||||
return $document->saveXML();
|
||||
}
|
||||
|
||||
public function exportFile(string $file, string $user, bool $flat = false): bool {
|
||||
$data = $this->export($user, $flat);
|
||||
if (!@file_put_contents($file, $data)) {
|
||||
// if it fails throw an exception
|
||||
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
|
||||
throw new Exception($err, ['file' => $file, 'format' => str_replace(__NAMESPACE__."\\", "", __CLASS__)]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function importFile(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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,24 +10,24 @@ use JKingWeb\Arsse\ImportExport\OPML;
|
|||
use JKingWeb\Arsse\ImportExport\Exception;
|
||||
use org\bovigo\vfs\vfsStream;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\ImportExport\OPML<extended> */
|
||||
class TestOPMLFile extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
/** @covers \JKingWeb\Arsse\ImportExport\AbstractImportExport */
|
||||
class TestFile extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected $vfs;
|
||||
protected $path;
|
||||
protected $opml;
|
||||
protected $proc;
|
||||
|
||||
public function setUp() {
|
||||
self::clearData();
|
||||
// create a mock OPML processor with stubbed underlying import/export routines
|
||||
$this->opml = \Phake::partialMock(OPML::class);
|
||||
\Phake::when($this->opml)->export->thenReturn("OPML_FILE");
|
||||
\Phake::when($this->opml)->import->thenReturn(true);
|
||||
$this->proc = \Phake::partialMock(OPML::class);
|
||||
\Phake::when($this->proc)->export->thenReturn("EXPORT_FILE");
|
||||
\Phake::when($this->proc)->import->thenReturn(true);
|
||||
$this->vfs = vfsStream::setup("root", null, [
|
||||
'exportGoodFile' => "",
|
||||
'exportGoodDir' => [],
|
||||
'exportBadFile' => "",
|
||||
'exportBadDir' => [],
|
||||
'importGoodFile' => "<opml/>",
|
||||
'importGoodFile' => "GOOD_FILE",
|
||||
'importBadFile' => "",
|
||||
]);
|
||||
$this->path = $this->vfs->url()."/";
|
||||
|
@ -40,7 +40,7 @@ class TestOPMLFile extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
public function tearDown() {
|
||||
$this->path = null;
|
||||
$this->vfs = null;
|
||||
$this->opml = null;
|
||||
$this->proc = null;
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
|
@ -50,13 +50,13 @@ class TestOPMLFile extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
try {
|
||||
if ($exp instanceof \JKingWeb\Arsse\AbstractException) {
|
||||
$this->assertException($exp);
|
||||
$this->opml->exportFile($path, $user, $flat);
|
||||
$this->proc->exportFile($path, $user, $flat);
|
||||
} else {
|
||||
$this->assertSame($exp, $this->opml->exportFile($path, $user, $flat));
|
||||
$this->assertSame("OPML_FILE", $this->vfs->getChild($file)->getContent());
|
||||
$this->assertSame($exp, $this->proc->exportFile($path, $user, $flat));
|
||||
$this->assertSame("EXPORT_FILE", $this->vfs->getChild($file)->getContent());
|
||||
}
|
||||
} finally {
|
||||
\Phake::verify($this->opml)->export($user, $flat);
|
||||
\Phake::verify($this->proc)->export($user, $flat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,12 +89,12 @@ class TestOPMLFile extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
try {
|
||||
if ($exp instanceof \JKingWeb\Arsse\AbstractException) {
|
||||
$this->assertException($exp);
|
||||
$this->opml->importFile($path, $user, $flat, $replace);
|
||||
$this->proc->importFile($path, $user, $flat, $replace);
|
||||
} else {
|
||||
$this->assertSame($exp, $this->opml->importFile($path, $user, $flat, $replace));
|
||||
$this->assertSame($exp, $this->proc->importFile($path, $user, $flat, $replace));
|
||||
}
|
||||
} finally {
|
||||
\Phake::verify($this->opml, \Phake::times((int) ($exp === true)))->import($user, "<opml/>", $flat, $replace);
|
||||
\Phake::verify($this->proc, \Phake::times((int) ($exp === true)))->import($user, "GOOD_FILE", $flat, $replace);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@
|
|||
convertWarningsToExceptions="false"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
stopOnError="true">
|
||||
forceCoversAnnotation="true"
|
||||
>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
|
@ -114,8 +115,8 @@
|
|||
<file>cases/CLI/TestCLI.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Import/Export">
|
||||
<file>cases/ImportExport/TestFile.php</file>
|
||||
<file>cases/ImportExport/TestOPML.php</file>
|
||||
<file>cases/ImportExport/TestOPMLFile.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
|
Loading…
Add table
Reference in a new issue