diff options
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/Context.php | 24 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 155 | ||||
| -rw-r--r-- | app/Models/EntryDAOPGSQL.php | 5 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 7 | ||||
| -rw-r--r-- | app/Models/UserConfiguration.php | 1 |
5 files changed, 132 insertions, 60 deletions
diff --git a/app/Models/Context.php b/app/Models/Context.php index 6634482d3..0c8161c8b 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -40,16 +40,17 @@ final class FreshRSS_Context { public static string $next_get = 'a'; public static int $state = 0; - /** - * @phpstan-var 'ASC'|'DESC' - */ + /** @var 'ASC'|'DESC' */ public static string $order = 'DESC'; + /** @var 'id'|'date'|'link'|'title'|'rand' */ + public static string $sort = 'id'; public static int $number = 0; public static int $offset = 0; public static FreshRSS_BooleanSearch $search; - public static string $first_id = ''; - public static string $next_id = ''; - public static string $id_max = ''; + /** @var numeric-string */ + public static string $continuation_id = '0'; + /** @var numeric-string */ + public static string $id_max = '0'; public static int $sinceHours = 0; public static bool $isCli = false; @@ -221,7 +222,7 @@ final class FreshRSS_Context { self::_get(Minz_Request::paramString('get') ?: 'a'); self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state; - $state_forced_by_user = Minz_Request::paramString('state') !== ''; + $state_forced_by_user = Minz_Request::paramString('state', true) !== ''; if (!$state_forced_by_user) { if (FreshRSS_Context::userConf()->show_fav_unread && (self::isCurrentGet('s') || self::isCurrentGet('T') || self::isTag())) { self::$state = FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_READ; @@ -235,8 +236,10 @@ final class FreshRSS_Context { } self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search')); - $order = Minz_Request::paramString('order') ?: FreshRSS_Context::userConf()->sort_order; + $order = Minz_Request::paramString('order', true) ?: FreshRSS_Context::userConf()->sort_order; self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; + $sort = Minz_Request::paramString('sort', true) ?: FreshRSS_Context::userConf()->sort; + self::$sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page; if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) { self::$number = max( @@ -244,7 +247,10 @@ final class FreshRSS_Context { FreshRSS_Context::userConf()->posts_per_page); } self::$offset = Minz_Request::paramInt('offset'); - self::$first_id = Minz_Request::paramString('next'); + $id_max = Minz_Request::paramString('idMax', true); + self::$id_max = ctype_digit($id_max) ? $id_max : '0'; + $continuation_id = Minz_Request::paramString('cid', true); + self::$continuation_id = ctype_digit($continuation_id) ? $continuation_id : '0'; self::$sinceHours = Minz_Request::paramInt('hours'); } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 4e7f532ac..6075b0759 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -27,6 +27,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return str_replace('INSERT INTO ', 'INSERT IGNORE INTO ', $sql); } + public static function sqlRandom(): string { + return 'RAND()'; + } + /** @return array{pattern?:string,matchType?:string} */ protected static function regexToSql(string $regex): array { if (preg_match('#^/(?P<pattern>.*)/(?P<matchType>[im]*)$#', $regex, $matches)) { @@ -511,7 +515,7 @@ SQL; $sql .= ')'; } - [$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters); $stm = $this->pdo->prepare($sql . $search); if ($stm === false || !$stm->execute(array_merge($values, $searchValues))) { @@ -552,7 +556,7 @@ AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=?) SQL; $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id]; - [$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters); $stm = $this->pdo->prepare($sql . $search); if ($stm === false || !$stm->execute(array_merge($values, $searchValues))) { @@ -594,7 +598,7 @@ SQL; . 'WHERE id_feed=? AND is_read <> ? AND id <= ?'; $values = [$is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax]; - [$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters); $stm = $this->pdo->prepare($sql . $search); if ($stm === false || !$stm->execute(array_merge($values, $searchValues))) { @@ -652,7 +656,7 @@ SQL; $values[] = $is_read ? 1 : 0; $values[] = $idMax; - [$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: 'e.', state: $state, filters: $filters); $stm = $this->pdo->prepare($sql . $search); if ($stm === false || !$stm->execute(array_merge($values, $searchValues))) { @@ -1107,13 +1111,15 @@ SQL; } /** + * @param numeric-string $id_min + * @param numeric-string $id_max + * @param 'id'|'date'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order * @return array{0:list<int|string>,1:string} - * @throws FreshRSS_EntriesGetter_Exception */ - protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null, - int $state = FreshRSS_Entry::STATE_ALL, - string $order = 'DESC', string $firstId = '', int $date_min = 0): array { + protected function sqlListEntriesWhere(string $alias = '', int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, + string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', + string $continuation_id = '0', string|int $continuation_value = 0): array { $search = ' '; $values = []; if ($state & FreshRSS_Entry::STATE_ANDS) { @@ -1146,21 +1152,42 @@ SQL; } } - switch ($order) { - case 'DESC': - case 'ASC': - break; - default: - throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!'); + if (!ctype_digit($id_min)) { + $id_min = '0'; + } + if (!ctype_digit($id_max)) { + $id_max = '0'; + } + if (!ctype_digit($continuation_id)) { + $continuation_id = '0'; + } + + if ($continuation_id !== '0' && $sort === 'id') { + if ($order === 'ASC') { + $id_min = $id_min === '0' ? $continuation_id : max($id_min, $continuation_id); + } else { + $id_max = $id_max === '0' ? $continuation_id : min($id_max, $continuation_id); + } } - if ($firstId !== '') { - $search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? '; - $values[] = $firstId; + + if ($id_max !== '0') { + $search .= 'AND ' . $alias . 'id <= ? '; + $values[] = $id_max; } - if ($date_min > 0) { + if ($id_min !== '0') { $search .= 'AND ' . $alias . 'id >= ? '; - $values[] = $date_min . '000000'; + $values[] = $id_min; + } + + if ($continuation_id !== '0' && in_array($sort, ['date', 'link', 'title'], true)) { + $sign = $order === 'ASC' ? '>' : '<'; + // Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128) + $search .= "AND ({$alias}{$sort} {$sign} ? OR ({$alias}{$sort} = ? AND {$alias}id {$sign}= ?)) "; + $values[] = $continuation_value; + $values[] = $continuation_value; + $values[] = $continuation_id; } + if ($filters !== null && count($filters->searches()) > 0) { [$filterValues, $filterSearch] = self::sqlBooleanSearch($alias, $filters); $filterSearch = trim($filterSearch); @@ -1174,15 +1201,19 @@ SQL; } /** - * @phpstan-param 'a'|'A'|'i'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type + * @param 'a'|'A'|'i'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID + * @param numeric-string $id_min + * @param numeric-string $id_max + * @param 'id'|'date'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order + * @param numeric-string $continuation_id * @return array{0:list<int|string>,1:string} * @throws FreshRSS_EntriesGetter_Exception */ - private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, - string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null, - int $date_min = 0): array { + private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, + string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', + string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): array { if (!$state) { $state = FreshRSS_Entry::STATE_ALL; } @@ -1231,7 +1262,11 @@ SQL; throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); } - [$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min); + $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; + $sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; + $orderBy = ($sort === 'rand' ? static::sqlRandom() : 'e.' . $sort); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: 'e.', state: $state, filters: $filters, id_min: $id_min, id_max: $id_max, + sort: $sort, order: $order, continuation_id: $continuation_id, continuation_value: $continuation_value); return [array_merge($values, $searchValues), 'SELECT ' . ($type === 'T' ? 'DISTINCT ' : '') @@ -1240,34 +1275,45 @@ SQL; . ($type === 't' || $type === 'T' ? 'INNER JOIN `_entrytag` et ON et.id_entry = e.id ' : '') . 'WHERE ' . $where . $search - . 'ORDER BY e.id ' . $order + . 'ORDER BY ' . $orderBy . ' ' . $order + . ($sort === 'id' ? '' : ', e.id ' . $order) // For keyset pagination . ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ . ($offset > 0 ? ' OFFSET ' . $offset : '') ]; } /** - * @phpstan-param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST'|'Z' $type - * @param 'ASC'|'DESC' $order + * @param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID + * @param numeric-string $id_min + * @param numeric-string $id_max + * @param 'id'|'date'|'link'|'title'|'rand' $sort + * @param 'ASC'|'DESC' $order + * @param numeric-string $continuation_id * @throws FreshRSS_EntriesGetter_Exception */ - private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, - string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null, - int $date_min = 0): PDOStatement|false { - [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min); + private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, + string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', + string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): PDOStatement|false { + $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; + $sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; - if ($order !== 'DESC' && $order !== 'ASC') { - $order = 'DESC'; - } + [$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, + continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); + + $orderBy = ($sort === 'rand' ? static::sqlRandom() : 'e0.' . $sort); $content = static::isCompressed() ? 'UNCOMPRESS(e0.content_bin) AS content' : 'e0.content'; $hash = static::sqlHexEncode('e0.hash'); $sql = <<<SQL SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link, e0.date, {$hash} AS hash, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes FROM `_entry` e0 INNER JOIN ({$sql}) e2 ON e2.id=e0.id -ORDER BY e0.id {$order} +ORDER BY {$orderBy} {$order} SQL; + if ($sort !== 'id') { + // For keyset pagination + $sql .= ', e0.id ' . $order; + } $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values)) { return $stm; @@ -1275,7 +1321,8 @@ SQL; $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { - return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min); + return $this->listWhereRaw($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, + continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); } Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)); return false; @@ -1283,16 +1330,21 @@ SQL; } /** - * @phpstan-param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST'|'Z' $type + * @param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID + * @param numeric-string $id_min + * @param numeric-string $id_max + * @param 'id'|'date'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order + * @param numeric-string $continuation_id * @return Traversable<FreshRSS_Entry> * @throws FreshRSS_EntriesGetter_Exception */ - public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, - string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', - ?FreshRSS_BooleanSearch $filters = null, int $date_min = 0): Traversable { - $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min); + public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, + string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', + string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): Traversable { + $stm = $this->listWhereRaw($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, + continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); if ($stm !== false) { while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { if (is_array($row)) { @@ -1305,6 +1357,7 @@ SQL; } /** + * For API. * @param array<numeric-string> $ids * @param 'ASC'|'DESC' $order * @return Traversable<FreshRSS_Entry> @@ -1317,15 +1370,13 @@ SQL; // Split a query with too many variables parameters $idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($idsChunks as $idsChunk) { - foreach ($this->listByIds($idsChunk, $order) as $entry) { + foreach ($this->listByIds($idsChunk, order: $order) as $entry) { yield $entry; } } return; } - if ($order !== 'DESC' && $order !== 'ASC') { - $order = 'DESC'; - } + $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; $content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content'; $hash = static::sqlHexEncode('hash'); $repeats = str_repeat('?,', count($ids) - 1) . '?'; @@ -1349,16 +1400,20 @@ SQL; } /** - * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type + * @param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID + * @param numeric-string $id_min + * @param numeric-string $id_max + * @param numeric-string $continuation_id * @param 'ASC'|'DESC' $order * @return list<numeric-string>|null * @throws FreshRSS_EntriesGetter_Exception */ - public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, - string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array { - - [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters); + public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, + string $id_min = '0', string $id_max = '0', string $order = 'DESC', + string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): ?array { + [$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, order: $order, + continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) { $res = array_map('strval', $res); diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index 1a5266bbd..c42c2cec1 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -24,6 +24,11 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { } #[\Override] + public static function sqlRandom(): string { + return 'RANDOM()'; + } + + #[\Override] protected static function sqlRegex(string $expression, string $regex, array &$values): string { $matches = static::regexToSql($regex); if (isset($matches['pattern'])) { diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 5734ec3b3..6951afb27 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -29,6 +29,11 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } #[\Override] + public static function sqlRandom(): string { + return 'RANDOM()'; + } + + #[\Override] protected static function sqlRegex(string $expression, string $regex, array &$values): string { $values[] = $regex; return "{$expression} REGEXP ?"; @@ -164,7 +169,7 @@ SQL; $values[] = $id; } - [$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state); + [$searchValues, $search] = $this->sqlListEntriesWhere(alias: 'e.', state: $state, filters: $filters); $stm = $this->pdo->prepare($sql . $search); if ($stm === false || !$stm->execute(array_merge($values, $searchValues))) { diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 4d465bf67..ce6f0149d 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -51,6 +51,7 @@ declare(strict_types=1); * @property int $simplify_over_n_feeds * @property bool $show_nav_buttons * @property 'ASC'|'DESC' $sort_order + * @property 'id'|'date'|'link'|'title'|'rand' $sort * @property array<string,array<string,string>> $sharing * @property array<string,string> $shortcuts * @property bool $sides_close_article |
