Fix regressions on some array structures (#7155)

regressions from https://github.com/FreshRSS/FreshRSS/pull/7131
fix https://github.com/FreshRSS/FreshRSS/issues/7154
This commit is contained in:
Alexandre Alapetite
2024-12-28 23:58:00 +01:00
committed by GitHub
parent 33cdfbb309
commit c29cbb7b8b
15 changed files with 88 additions and 71 deletions

View File

@@ -218,7 +218,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
Minz_Error::error(404);
return;
}
$this->view->categories = [ $cat ];
$this->view->categories = [$cat->id() => $cat];
break;
case 'f':
// We most likely already have the feed object in cache
@@ -231,7 +231,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
return;
}
}
$this->view->feeds = [ $feed ];
$this->view->feeds = [$feed->id() => $feed];
break;
case 's':
case 't':

View File

@@ -93,16 +93,18 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeeds();
$id = Minz_Request::paramInt('id');
if ($id === 0 || !isset($this->view->feeds[$id])) {
Minz_Error::error(404);
if ($id === 0) {
Minz_Error::error(400);
return;
}
$feed = $this->view->feeds[$id];
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
if ($feed === null) {
Minz_Error::error(404);
return;
}
$this->view->feed = $feed;
FreshRSS_View::prependTitle($feed->name() . ' · ' . _t('sub.title.feed_management') . ' · ');

View File

@@ -19,7 +19,7 @@ class FreshRSS_Category extends Minz_Model {
private string $name;
private int $nbFeeds = -1;
private int $nbNotRead = -1;
/** @var list<FreshRSS_Feed>|null */
/** @var array<int,FreshRSS_Feed>|null where the key is the feed ID */
private ?array $feeds = null;
/** @var bool|int */
private $hasFeedsWithError = false;
@@ -100,7 +100,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
* @return list<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed> where the key is the feed ID
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@@ -157,9 +157,11 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
$this->feeds = [];
}
$feed->_category($this);
$this->feeds[] = $feed;
$this->sortFeeds();
if ($feed->id() !== 0) {
$feed->_category($this);
$this->feeds[$feed->id()] = $feed;
$this->sortFeeds();
}
}
/**
@@ -243,7 +245,7 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
usort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
uasort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
}
/**
@@ -265,13 +267,13 @@ class FreshRSS_Category extends Minz_Model {
/**
* Access cached feeds
* @param array<FreshRSS_Category> $categories
* @return list<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed> where the key is the feed ID
*/
public static function findFeeds(array $categories): array {
$result = [];
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
$result[] = $feed;
$result[$feed->id()] = $feed;
}
}
return $result;

View File

@@ -257,11 +257,11 @@ SQL;
return reset($categories) ?: null;
}
/** @return list<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> where the key is the category ID */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
usort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
$aPosition = $a->attributeInt('position');
$bPosition = $b->attributeInt('position');
if ($aPosition === $bPosition) {
@@ -277,7 +277,7 @@ SQL;
return $categories;
}
/** @return list<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> where the key is the category ID */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
@@ -310,7 +310,7 @@ SQL;
}
}
/** @return list<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> where the key is the category ID */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@@ -338,8 +338,8 @@ SQL;
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $res */
$categories = self::daoToCategories($res); // @phpstan-ignore varTag.type
if (isset($categories[0])) {
return $categories[0];
if (isset($categories[self::DEFAULTCATEGORYID])) {
return $categories[self::DEFAULTCATEGORYID];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@@ -415,7 +415,7 @@ SQL;
* @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
* id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int,
* error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $listDAO
* @return list<FreshRSS_Category>
* @return array<int,FreshRSS_Category> where the key is the category ID
*/
private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
@@ -432,7 +432,7 @@ SQL;
);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
$list[] = $cat;
$list[$cat->id()] = $cat;
$feedsDao = []; //Prepare for next category
}
@@ -452,7 +452,7 @@ SQL;
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes($previousLine['c_attributes'] ?? []);
$list[] = $cat;
$list[$cat->id()] = $cat;
}
return $list;
@@ -460,7 +460,7 @@ SQL;
/**
* @param array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $listDAO
* @return list<FreshRSS_Category>
* @return array<int,FreshRSS_Category> where the key is the category ID
*/
private static function daoToCategories(array $listDAO): array {
$list = [];
@@ -473,7 +473,7 @@ SQL;
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes($dao['attributes'] ?? '');
$list[] = $cat;
$list[$cat->id()] = $cat;
}
return $list;
}

View File

@@ -7,13 +7,9 @@ declare(strict_types=1);
*/
final class FreshRSS_Context {
/**
* @var list<FreshRSS_Category>
*/
/** @var array<int,FreshRSS_Category> where the key is the category ID */
private static array $categories = [];
/**
* @var list<FreshRSS_Tag>
*/
/** @var array<int,FreshRSS_Tag> where the key is the label ID */
private static array $tags = [];
public static string $name = '';
public static string $description = '';
@@ -176,7 +172,7 @@ final class FreshRSS_Context {
FreshRSS_Context::$user_conf = null;
}
/** @return list<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> where the key is the category ID */
public static function categories(): array {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
@@ -185,12 +181,12 @@ final class FreshRSS_Context {
return self::$categories;
}
/** @return list<FreshRSS_Feed> */
/** @return array<int,FreshRSS_Feed> where the key is the feed ID */
public static function feeds(): array {
return FreshRSS_Category::findFeeds(self::categories());
}
/** @return list<FreshRSS_Tag> */
/** @return array<int,FreshRSS_Tag> where the key is the label ID */
public static function labels(bool $precounts = false): array {
if (empty(self::$tags) || $precounts) {
$tagDAO = FreshRSS_Factory::createTagDao();

View File

@@ -325,7 +325,7 @@ SQL;
return null;
}
$feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type
return $feeds[0] ?? null;
return $feeds[$id] ?? null;
}
public function searchByUrl(string $url): ?FreshRSS_Feed {
@@ -342,9 +342,7 @@ SQL;
return $res;
}
/**
* @return list<FreshRSS_Feed>
*/
/** @return array<int,FreshRSS_Feed> where the key is the feed ID */
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
@@ -373,7 +371,7 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
* @return list<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed> where the key is the feed ID
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
@@ -409,7 +407,7 @@ SQL;
/**
* @param bool|null $muted to include only muted feeds
* @param bool|null $errored to include only errored feeds
* @return list<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed> where the key is the feed ID
*/
public function listByCategory(int $cat, ?bool $muted = null, ?bool $errored = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@@ -424,7 +422,7 @@ SQL;
return [];
}
$feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type
usort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
uasort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
return $feeds;
}
@@ -569,7 +567,7 @@ SQL;
/**
* @param array<array{id?:int,url?:string,kind?:int,category?:int,name?:string,website?:string,description?:string,lastUpdate?:int,priority?:int,
* pathEntries?:string,httpAuth?:string,error?:int|bool,ttl?:int,attributes?:string,cache_nbUnreads?:int,cache_nbEntries?:int}> $listDAO
* @return list<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed> where the key is the feed ID
*/
public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
@@ -602,7 +600,7 @@ SQL;
if (isset($dao['id'])) {
$myFeed->_id($dao['id']);
}
$list[] = $myFeed;
$list[$myFeed->id()] = $myFeed;
}
return $list;

View File

@@ -182,7 +182,7 @@ SQL;
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
/** @return list<FreshRSS_Tag>|false */
/** @return array<int,FreshRSS_Tag>|false where the key is the label ID */
public function listTags(bool $precounts = false): array|false {
if ($precounts) {
$sql = <<<'SQL'
@@ -408,7 +408,7 @@ SQL;
/**
* @param iterable<array{id:int,name:string,attributes?:string}> $listDAO
* @return list<FreshRSS_Tag>
* @return array<int,FreshRSS_Tag> where the key is the label ID
*/
private static function daoToTags(iterable $listDAO): array {
$list = [];
@@ -424,7 +424,7 @@ SQL;
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
$list[] = $tag;
$list[$tag->id()] = $tag;
}
return $list;
}

View File

@@ -21,6 +21,10 @@ class FreshRSS_UserQuery {
private string $token = '';
private bool $shareRss = false;
private bool $shareOpml = false;
/** @var array<int,FreshRSS_Category> $categories where the key is the category ID */
private array $categories;
/** @var array<int,FreshRSS_Tag> $labels where the key is the label ID */
private array $labels;
/** XML-encoded description */
private string $description = '';
private string $imageUrl = '';
@@ -40,14 +44,18 @@ class FreshRSS_UserQuery {
/**
* @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,
* shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string} $query
* @param array<int,FreshRSS_Category> $categories
* @param array<int,FreshRSS_Tag> $labels
* @param array<FreshRSS_Category> $categories
* @param array<FreshRSS_Tag> $labels
*/
public function __construct(
array $query,
private array $categories,
private array $labels,
) {
public function __construct(array $query, array $categories, array $labels) {
$this->categories = [];
foreach ($categories as $category) {
$this->categories[$category->id()] = $category;
}
$this->labels = [];
foreach ($labels as $label) {
$this->labels[$label->id()] = $label;
}
if (isset($query['get'])) {
$this->parseGet($query['get']);
} else {

View File

@@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
/** @var list<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> where the key is the category ID */
public array $categories;
public ?FreshRSS_Category $category = null;
public ?FreshRSS_Tag $tag = null;
@@ -19,10 +19,10 @@ class FreshRSS_View extends Minz_View {
public $entries;
public ?FreshRSS_Entry $entry = null;
public ?FreshRSS_Feed $feed = null;
/** @var list<FreshRSS_Feed> */
/** @var array<int,FreshRSS_Feed> where the key is the feed ID */
public array $feeds;
public int $nbUnreadTags;
/** @var list<FreshRSS_Tag> */
/** @var array<int,FreshRSS_Tag> where the key is the label ID */
public array $tags;
/** @var array<int,array{id:int,name:string,checked:bool}> */
public array $tagsForEntry;
@@ -70,7 +70,7 @@ class FreshRSS_View extends Minz_View {
public array $list_keys;
// User queries
/** @var array<int,FreshRSS_UserQuery> */
/** @var array<int,FreshRSS_UserQuery> where the key is the query ID */
public array $queries;
/** @var FreshRSS_UserQuery|null */
public ?FreshRSS_UserQuery $query = null;

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
/** @var list<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> where the key is the category ID */
public array $categories;
/** @var list<FreshRSS_Feed> */
/** @var array<int,FreshRSS_Feed> where the key is the feed ID */
public array $feeds;
/** @var list<FreshRSS_Tag> */
/** @var array<int,FreshRSS_Tag> where the key is the label ID */
public array $tags;
public string $nonce;

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
final class FreshRSS_ViewStats extends FreshRSS_View {
/** @var list<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> where the key is the category ID */
public array $categories;
public ?FreshRSS_Feed $feed = null;
/** @var list<FreshRSS_Feed> */
/** @var array<int,FreshRSS_Feed> where the key is the feed ID */
public array $feeds;
public bool $displaySlider = false;

View File

@@ -149,7 +149,7 @@ class FreshRSS_Import_Service {
try {
// Create a Feed object and add it in DB
$feed = new FreshRSS_Feed($url);
$category->addFeed($feed);
$feed->_category($category);
$feed->_name($name);
$feed->_website($website);
$feed->_description($description);
@@ -319,6 +319,7 @@ class FreshRSS_Import_Service {
$this->lastStatus = false;
} else {
$feed->_id($id);
$category->addFeed($feed);
return $feed;
}
}

View File

@@ -134,7 +134,7 @@ switch ($type) {
Minz_Error::error(404, "Category {$id} not found!");
die();
}
$view->categories = [ $cat ];
$view->categories = [$cat->id() => $cat];
break;
case 'f': // Feed
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);

View File

@@ -33,6 +33,7 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feed_1 = $this->getMockBuilder(FreshRSS_Feed::class)
->disableOriginalConstructor()
->getMock();
$feed_1->method('id')->withAnyParameters()->willReturn(1);
$feed_1->expects(self::any())
->method('name')
->willReturn('AAA');
@@ -40,6 +41,7 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feed_2 = $this->getMockBuilder(FreshRSS_Feed::class)
->disableOriginalConstructor()
->getMock();
$feed_2->method('id')->withAnyParameters()->willReturn(2);
$feed_2->expects(self::any())
->method('name')
->willReturn('ZZZ');
@@ -47,6 +49,7 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feed_3 = $this->getMockBuilder(FreshRSS_Feed::class)
->disableOriginalConstructor()
->getMock();
$feed_3->method('id')->withAnyParameters()->willReturn(3);
$feed_3->expects(self::any())
->method('name')
->willReturn('lll');
@@ -70,9 +73,11 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feed_4 = $this->getMockBuilder(FreshRSS_Feed::class)
->disableOriginalConstructor()
->getMock();
$feed_4->method('id')->withAnyParameters()->willReturn(4);
$feed_4->expects(self::any())
->method('name')
->willReturn('BBB');
$feed_4->method('id')->withAnyParameters()->willReturn(5);
$category->addFeed($feed_4);
$feeds = $category->feeds();

View File

@@ -23,12 +23,13 @@ class UserQueryTest extends TestCase {
$category_name = 'some category name';
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
$cat->method('id')->withAnyParameters()->willReturn(1);
$cat->expects(self::atLeastOnce())
->method('name')
->withAnyParameters()
->willReturn($category_name);
$query = ['get' => 'c_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
$user_query = new FreshRSS_UserQuery($query, [$cat], []);
self::assertSame($category_name, $user_query->getGetName());
self::assertSame('category', $user_query->getGetType());
}
@@ -47,12 +48,13 @@ class UserQueryTest extends TestCase {
->willReturn($feed_name);
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
$cat->method('id')->withAnyParameters()->willReturn(1);
$cat->expects(self::atLeastOnce())
->method('feeds')
->withAnyParameters()
->willReturn([1 => $feed]);
$query = ['get' => 'f_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
$user_query = new FreshRSS_UserQuery($query, [$cat], []);
self::assertSame($feed_name, $user_query->getGetName());
self::assertSame('feed', $user_query->getGetType());
}
@@ -143,12 +145,13 @@ class UserQueryTest extends TestCase {
public function testIsDeprecated_whenCategoryExists_returnFalse(): void {
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
$cat->method('id')->withAnyParameters()->willReturn(1);
$cat->expects(self::atLeastOnce())
->method('name')
->withAnyParameters()
->willReturn('cat 1');
$query = ['get' => 'c_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
$user_query = new FreshRSS_UserQuery($query, [$cat], []);
self::assertFalse($user_query->isDeprecated());
}
@@ -171,24 +174,26 @@ class UserQueryTest extends TestCase {
->willReturn('feed 1');
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
$cat->method('id')->withAnyParameters()->willReturn(1);
$cat->expects(self::atLeastOnce())
->method('feeds')
->withAnyParameters()
->willReturn([1 => $feed]);
$query = ['get' => 'f_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
$user_query = new FreshRSS_UserQuery($query, [$cat], []);
self::assertFalse($user_query->isDeprecated());
}
public function testIsDeprecated_whenFeedDoesNotExist_returnTrue(): void {
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
$cat->method('id')->withAnyParameters()->willReturn(1);
$cat->expects(self::atLeastOnce())
->method('feeds')
->withAnyParameters()
->willReturn([]);
$query = ['get' => 'f_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
$user_query = new FreshRSS_UserQuery($query, [$cat], []);
self::assertTrue($user_query->isDeprecated());
}