mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-23 09:02:41 +00:00
Fix up the aadding of subscription
Tests to come. Because a feed could be added without adding a subscription previously, it was possible to check feeds for validity before adding subscriptions, yielding visibly atomic operations. These new functions replicate this functionality by adding soft-deleted subscriptions and undeleting them once the fetch succeeds.
This commit is contained in:
parent
2c9daedb14
commit
9f784251e8
2 changed files with 159 additions and 106 deletions
128
lib/Database.php
128
lib/Database.php
|
@ -770,18 +770,94 @@ class Database {
|
||||||
|
|
||||||
/** Adds a subscription to a newsfeed, and returns the numeric identifier of the added subscription
|
/** Adds a subscription to a newsfeed, and returns the numeric identifier of the added subscription
|
||||||
*
|
*
|
||||||
* @param string $user The user which will own the subscription
|
* This is an all-in-one operation which reserves an ID, sets the subscription's
|
||||||
|
* properties, and based on whether the feed is fetched successfully, either makes
|
||||||
|
* the subscription available to the user, or deletes the reservation.
|
||||||
|
*
|
||||||
|
* @param string $user The user who will own the subscription
|
||||||
* @param string $url The URL of the newsfeed or discovery source
|
* @param string $url The URL of the newsfeed or discovery source
|
||||||
* @param string $fetchUser The user name required to access the newsfeed, if applicable
|
* @param string $fetchUser The user name required to access the newsfeed, if applicable
|
||||||
* @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext
|
* @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext
|
||||||
* @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document
|
* @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document
|
||||||
* @param boolean $scrape Whether the initial synchronization should scrape full-article content
|
* @param array $properties An associative array of properties accepted by the `subscriptionPropertiesSet` function
|
||||||
*/
|
*/
|
||||||
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true, bool $scrape = false): int {
|
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true, array $properties = []): int {
|
||||||
// get the ID of the underlying feed, or add it if it's not yet in the database
|
$id = $this->subscriptionReserve($user, $url, $fetchUser, $fetchPassword, $discover);
|
||||||
$feedID = $this->feedAdd($url, $fetchUser, $fetchPassword, $discover, $scrape);
|
try {
|
||||||
// Add the feed to the user's subscriptions and return the new subscription's ID.
|
if ($properties) {
|
||||||
return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId();
|
$this->subscriptionPropertiesSet($user, $id, $properties);
|
||||||
|
}
|
||||||
|
$this->subscriptionUpdate($user, $id, true);
|
||||||
|
$this->subscriptionReveal($user, $id);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// if the process failed, the subscription should be deleted immediately rather than cluttering up the database
|
||||||
|
$this->db->prepare("DELETE from arsse_subscriptions where id = ?", "int")->run($id);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a subscription to the database without exposing it to the user, returning its ID
|
||||||
|
*
|
||||||
|
* If the subscription already exists in the database an exception is thrown, unless the
|
||||||
|
* subscription was soft-deleted; in this case the the existing ID is returned without
|
||||||
|
* clearing the delete flag
|
||||||
|
*
|
||||||
|
* This function can used with `subscriptionUpdate` and `subscriptionReveal` to simulate
|
||||||
|
* atomic addition (or rollback) of multiple newsfeeds, something which is not normally
|
||||||
|
* practical due to network retrieval and processing times
|
||||||
|
*
|
||||||
|
* @param string $user The user who will own the subscription
|
||||||
|
* @param string $url The URL of the newsfeed or discovery source
|
||||||
|
* @param string $fetchUser The user name required to access the newsfeed, if applicable
|
||||||
|
* @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext
|
||||||
|
* @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document
|
||||||
|
*/
|
||||||
|
public function subscriptionReserve(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int {
|
||||||
|
// normalize the input URL
|
||||||
|
$url = URL::normalize($url, $fetchUser, $fetchPassword);
|
||||||
|
// if discovery is enabled, check to see if the feed already exists; this will save us some network latency if it does
|
||||||
|
if ($discover) {
|
||||||
|
$id = $this->db->prepare("SELECT id from arsse_subscriptions where ownerr = ? and url = ?", "str", "str")->run($user, $url)->getValue();
|
||||||
|
if (!$id) {
|
||||||
|
// if it doesn't exist, perform discovery
|
||||||
|
$url = Feed::discover($url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (int) $this->db->prepare('INSERT INTO arsse_feeds(owner, url, deleted) values(?,?,?)', 'str', 'str', 'bool')->run($user, $url, 1)->lastId();
|
||||||
|
} catch (Db\ExceptionInput $e) {
|
||||||
|
// if the insertion fails, throw if the delete flag is not set, otherwise return the existing ID
|
||||||
|
$id = (int) $this->db->prepare("SELECT id from arsse_subscriptions where owner = ? and url = ? and deleted = 1")->run($user, $url)->getValue();
|
||||||
|
if (!$id) {
|
||||||
|
throw $e;
|
||||||
|
} else {
|
||||||
|
// set the modification timestamp to the current time so it doesn't get cleaned up too soon
|
||||||
|
$this->db->prepare("UPDATE arsse_subscriptions set modified = CURRENT_TIMESTAMP where id = ?", "int")->run($id);
|
||||||
|
}
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attempts to refresh a subscribed newsfeed, returning an indication of success
|
||||||
|
*
|
||||||
|
* @param string|null $user The user whose subscribed newsfeed is to be updated; this may be null to facilitate refreshing feeds from the CLI
|
||||||
|
* @param integer $id The numerical identifier of the subscription to refresh
|
||||||
|
* @param boolean $throwError Whether to throw an exception on failure in addition to storing error information in the database
|
||||||
|
*/
|
||||||
|
public function subscriptionUpdate(?string $user, int $id, bool $throwError = false): bool {
|
||||||
|
// TODO: stub
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the soft-delete flag from one or more subscriptions, making them visible to the user
|
||||||
|
*
|
||||||
|
* @param string $user The user whose subscriptions to reveal
|
||||||
|
* @param int $id The numerical identifier(s) of the subscription(s) to reveal
|
||||||
|
*/
|
||||||
|
public function subscriptionReveal(string $user, int ...$id): void {
|
||||||
|
[$inClause, $inTypes, $inValues] = $this->generateIn($id, "int");
|
||||||
|
$this->db->prepare("UPDATE arsse_subscriptions set deleted = 0, modified = CURRENT_TIMESTAMP where deleted = 1 and user = ? and id in ($inClause)", "str", $inTypes)->run($user, $inValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lists a user's subscriptions, returning various data
|
/** Lists a user's subscriptions, returning various data
|
||||||
|
@ -997,7 +1073,7 @@ class Database {
|
||||||
'pinned' => "strict bool",
|
'pinned' => "strict bool",
|
||||||
'keep_rule' => "str",
|
'keep_rule' => "str",
|
||||||
'block_rule' => "str",
|
'block_rule' => "str",
|
||||||
'scrape' => "bool",
|
'scrape' => "strict bool",
|
||||||
];
|
];
|
||||||
[$setClause, $setTypes, $setValues] = $this->generateSet($data, $valid);
|
[$setClause, $setTypes, $setValues] = $this->generateSet($data, $valid);
|
||||||
if (!$setClause) {
|
if (!$setClause) {
|
||||||
|
@ -1132,42 +1208,6 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Adds a newsfeed to the database without adding any subscriptions, and returns the numeric identifier of the added feed
|
|
||||||
*
|
|
||||||
* If the feed already exists in the database, the existing ID is returned
|
|
||||||
*
|
|
||||||
* @param string $url The URL of the newsfeed or discovery source
|
|
||||||
* @param string $fetchUser The user name required to access the newsfeed, if applicable
|
|
||||||
* @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext
|
|
||||||
* @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document
|
|
||||||
* @param boolean $scrape Whether the initial synchronization should scrape full-article content
|
|
||||||
*/
|
|
||||||
public function feedAdd(string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true, bool $scrape = false): int {
|
|
||||||
// normalize the input URL
|
|
||||||
$url = URL::normalize($url);
|
|
||||||
// check to see if the feed already exists
|
|
||||||
$check = $this->db->prepare("SELECT id from arsse_feeds where url = ? and username = ? and password = ?", "str", "str", "str");
|
|
||||||
$feedID = $check->run($url, $fetchUser, $fetchPassword)->getValue();
|
|
||||||
if ($discover && is_null($feedID)) {
|
|
||||||
// if the feed doesn't exist, first perform discovery if requested and check for the existence of that URL
|
|
||||||
$url = Feed::discover($url, $fetchUser, $fetchPassword);
|
|
||||||
$feedID = $check->run($url, $fetchUser, $fetchPassword)->getValue();
|
|
||||||
}
|
|
||||||
if (is_null($feedID)) {
|
|
||||||
// if the feed still doesn't exist in the database, add it to the database; we do this unconditionally so as to lock SQLite databases for as little time as possible
|
|
||||||
$feedID = $this->db->prepare('INSERT INTO arsse_feeds(url,username,password) values(?,?,?)', 'str', 'str', 'str')->run($url, $fetchUser, $fetchPassword)->lastId();
|
|
||||||
try {
|
|
||||||
// perform an initial update on the newly added feed
|
|
||||||
$this->feedUpdate($feedID, true, $scrape);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// if the update fails, delete the feed we just added
|
|
||||||
$this->db->prepare('DELETE from arsse_feeds where id = ?', 'int')->run($feedID);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (int) $feedID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an indexed array of numeric identifiers for newsfeeds which should be refreshed */
|
/** Returns an indexed array of numeric identifiers for newsfeeds which should be refreshed */
|
||||||
public function feedListStale(): array {
|
public function feedListStale(): array {
|
||||||
$feeds = $this->db->query("SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP")->getAll();
|
$feeds = $this->db->query("SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP")->getAll();
|
||||||
|
|
|
@ -41,24 +41,15 @@ trait SeriesSubscription {
|
||||||
[2,"http://example.net/favicon.ico", null],
|
[2,"http://example.net/favicon.ico", null],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'arsse_feeds' => [
|
|
||||||
'columns' => ["id", "url", "title", "username", "password", "updated", "next_fetch", "icon"],
|
|
||||||
'rows' => [
|
|
||||||
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null],
|
|
||||||
[2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1],
|
|
||||||
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),2],
|
|
||||||
[4,"http://example.com/feed4", "Foo", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'arsse_subscriptions' => [
|
'arsse_subscriptions' => [
|
||||||
'columns' => ["id", "owner", "feed", "title", "folder", "pinned", "order_type", "keep_rule", "block_rule", "scrape"],
|
'columns' => ["id", "owner", "url", "feed_title", "updated", "next_fetch", "icon", "title", "folder", "pinned", "order_type", "keep_rule", "block_rule", "scrape"],
|
||||||
'rows' => [
|
'rows' => [
|
||||||
[1,"john.doe@example.com",2,null,null,1,2,null,null,0],
|
[1, "john.doe@example.com", "http://example.com/feed2", "eek", strtotime("now - 1 hour"), strtotime("now - 1 hour"), 1, null, null, 1, 2, null, null, 0],
|
||||||
[2,"jane.doe@example.com",2,null,null,0,0,null,null,0],
|
[2, "jane.doe@example.com", "http://example.com/feed2", "eek", strtotime("now - 1 hour"), strtotime("now - 1 hour"), 1, null, null, 0, 0, null, null, 0],
|
||||||
[3,"john.doe@example.com",3,"Ook",2,0,1,null,null,0],
|
[3, "john.doe@example.com", "http://example.com/feed3", "Ack", strtotime("now + 1 hour"), strtotime("now + 1 hour"), 2, "Ook", 2, 0, 1, null, null, 0],
|
||||||
[4,"jill.doe@example.com",2,null,null,0,0,null,null,0],
|
[4, "jill.doe@example.com", "http://example.com/feed2", "eek", strtotime("now - 1 hour"), strtotime("now - 1 hour"), 1, null, null, 0, 0, null, null, 0],
|
||||||
[5,"jack.doe@example.com",2,null,null,1,2,"","3|E",0],
|
[5, "jack.doe@example.com", "http://example.com/feed2", "eek", strtotime("now - 1 hour"), strtotime("now - 1 hour"), 1, null, null, 1, 2, "", "3|E", 0],
|
||||||
[6,"john.doe@example.com",4,"Bar",3,0,0,null,null,0],
|
[6, "john.doe@example.com", "http://example.com/feed4", "Foo", strtotime("now + 1 hour"), strtotime("now + 1 hour"), null, "Bar", 3, 0, 0, null, null, 0],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'arsse_tags' => [
|
'arsse_tags' => [
|
||||||
|
@ -81,16 +72,31 @@ trait SeriesSubscription {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'arsse_articles' => [
|
'arsse_articles' => [
|
||||||
'columns' => ["id", "feed", "url_title_hash", "url_content_hash", "title_content_hash", "title"],
|
'columns' => ["id", "subscription", "url_title_hash", "url_content_hash", "title_content_hash", "title", "read", "starred", "hidden"],
|
||||||
'rows' => [
|
'rows' => [
|
||||||
[1,2,"","","","Title 1"],
|
[1, 1, "", "", "", "Title 1", 1, 0, 0],
|
||||||
[2,2,"","","","Title 2"],
|
[2, 1, "", "", "", "Title 2", 0, 0, 0],
|
||||||
[3,2,"","","","Title 3"],
|
[3, 1, "", "", "", "Title 3", 0, 0, 0],
|
||||||
[4,2,"","","","Title 4"],
|
[4, 1, "", "", "", "Title 4", 0, 0, 0],
|
||||||
[5,2,"","","","Title 5"],
|
[5, 1, "", "", "", "Title 5", 0, 0, 0],
|
||||||
[6,3,"","","","Title 6"],
|
[6, 2, "", "", "", "Title 1", 1, 0, 0],
|
||||||
[7,3,"","","","Title 7"],
|
[7, 2, "", "", "", "Title 2", 1, 0, 0],
|
||||||
[8,3,"","","","Title 8"],
|
[8, 2, "", "", "", "Title 3", 1, 0, 0],
|
||||||
|
[9, 2, "", "", "", "Title 4", 1, 0, 0],
|
||||||
|
[10, 2, "", "", "", "Title 5", 1, 0, 0],
|
||||||
|
[11, 4, "", "", "", "Title 1", 0, 0, 0],
|
||||||
|
[12, 4, "", "", "", "Title 2", 0, 0, 0],
|
||||||
|
[13, 4, "", "", "", "Title 3", 0, 0, 0],
|
||||||
|
[14, 4, "", "", "", "Title 4", 0, 0, 0],
|
||||||
|
[15, 4, "", "", "", "Title 5", 0, 0, 0],
|
||||||
|
[16, 5, "", "", "", "Title 1", 1, 0, 0],
|
||||||
|
[17, 5, "", "", "", "Title 2", 0, 0, 0],
|
||||||
|
[18, 5, "", "", "", "Title 3", 1, 0, 1],
|
||||||
|
[19, 5, "", "", "", "Title 4", 0, 0, 0],
|
||||||
|
[20, 5, "", "", "", "Title 5", 0, 0, 1],
|
||||||
|
[21, 3, "", "", "", "Title 6", 0, 0, 0],
|
||||||
|
[22, 3, "", "", "", "Title 7", 1, 0, 0],
|
||||||
|
[23, 3, "", "", "", "Title 8", 0, 0, 0],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'arsse_editions' => [
|
'arsse_editions' => [
|
||||||
|
@ -104,6 +110,21 @@ trait SeriesSubscription {
|
||||||
[6, 6],
|
[6, 6],
|
||||||
[7, 7],
|
[7, 7],
|
||||||
[8, 8],
|
[8, 8],
|
||||||
|
[9, 9],
|
||||||
|
[10, 10],
|
||||||
|
[11, 11],
|
||||||
|
[12, 12],
|
||||||
|
[13, 13],
|
||||||
|
[14, 14],
|
||||||
|
[15, 15],
|
||||||
|
[16, 16],
|
||||||
|
[17, 17],
|
||||||
|
[18, 18],
|
||||||
|
[19, 19],
|
||||||
|
[20, 20],
|
||||||
|
[21, 21],
|
||||||
|
[22, 22],
|
||||||
|
[23, 23],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'arsse_categories' => [
|
'arsse_categories' => [
|
||||||
|
@ -113,26 +134,21 @@ trait SeriesSubscription {
|
||||||
[2, "B"],
|
[2, "B"],
|
||||||
[4, "D"],
|
[4, "D"],
|
||||||
[5, "E"],
|
[5, "E"],
|
||||||
[6,"F"],
|
[6, "A"],
|
||||||
[7,"G"],
|
[7, "B"],
|
||||||
[8,"H"],
|
[9, "D"],
|
||||||
],
|
[10, "E"],
|
||||||
],
|
[11, "A"],
|
||||||
'arsse_marks' => [
|
[12, "B"],
|
||||||
'columns' => ["article", "subscription", "read", "starred", "hidden"],
|
[14, "D"],
|
||||||
'rows' => [
|
[15, "E"],
|
||||||
[1,2,1,0,0],
|
[16, "A"],
|
||||||
[2,2,1,0,0],
|
[17, "B"],
|
||||||
[3,2,1,0,0],
|
[19, "D"],
|
||||||
[4,2,1,0,0],
|
[20, "E"],
|
||||||
[5,2,1,0,0],
|
[21, "F"],
|
||||||
[1,1,1,0,0],
|
[22, "G"],
|
||||||
[7,3,1,0,0],
|
[23, "H"],
|
||||||
[8,3,0,0,0],
|
|
||||||
[1,5,1,0,0],
|
|
||||||
[3,5,1,0,1],
|
|
||||||
[4,5,0,0,0],
|
|
||||||
[5,5,0,0,1],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@ -151,11 +167,8 @@ trait SeriesSubscription {
|
||||||
Arsse::$db = $db->get();
|
Arsse::$db = $db->get();
|
||||||
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
|
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
|
||||||
$db->feedUpdate->never()->called();
|
$db->feedUpdate->never()->called();
|
||||||
$state = $this->primeExpectations($this->data, [
|
$state = $this->primeExpectations($this->data, ['arsse_subscriptions' => ['id', 'owner', 'feed', 'url']]);
|
||||||
'arsse_feeds' => ['id','url','username','password'],
|
$state['arsse_subscriptions']['rows'][] = [$subID, $this->user, "http://example.com/feed1"];
|
||||||
'arsse_subscriptions' => ['id','owner','feed'],
|
|
||||||
]);
|
|
||||||
$state['arsse_subscriptions']['rows'][] = [$subID,$this->user,1];
|
|
||||||
$this->compareExpectations(static::$drv, $state);
|
$this->compareExpectations(static::$drv, $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue