aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Category.php7
-rw-r--r--app/Models/Context.php48
-rw-r--r--app/Models/EntryDAO.php126
-rw-r--r--app/Models/Feed.php7
-rw-r--r--app/Models/UserConfiguration.php4
5 files changed, 157 insertions, 35 deletions
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 2bdad2904..08ab22c19 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -171,6 +171,13 @@ class FreshRSS_Category extends Minz_Model {
$this->sortFeeds();
}
+ public function defaultSort(): ?string {
+ return $this->attributeString('defaultSort');
+ }
+ public function defaultOrder(): ?string {
+ return $this->attributeString('defaultOrder');
+ }
+
/**
* To manually add feeds to this category (not committing to database).
*/
diff --git a/app/Models/Context.php b/app/Models/Context.php
index c9d743e76..7bd028780 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -42,8 +42,12 @@ final class FreshRSS_Context {
public static int $state = 0;
/** @var 'ASC'|'DESC' */
public static string $order = 'DESC';
- /** @var 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' */
+ /** @var 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' */
public static string $sort = 'id';
+ /** @var 'ASC'|'DESC' */
+ public static string $secondary_sort_order = 'DESC';
+ /** @var 'id'|'date'|'link'|'title' */
+ public static string $secondary_sort = 'id';
public static int $number = 0;
public static int $offset = 0;
public static FreshRSS_BooleanSearch $search;
@@ -258,10 +262,46 @@ final class FreshRSS_Context {
}
self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search', plaintext: true));
- $order = Minz_Request::paramString('order', plaintext: true) ?: FreshRSS_Context::userConf()->sort_order;
+
+ $default_order = null;
+ $default_sort = null;
+ if (Minz_Request::paramString('order', plaintext: true) === '' || Minz_Request::paramString('sort', plaintext: true) === '') {
+ if (!empty(self::$current_get['feed'])) {
+ $id = self::$current_get['feed'];
+ // We most likely already have the feed object in cache
+ $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
+ if ($feed === null) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feed = $feedDAO->searchById($id);
+ }
+ $default_order = $feed?->defaultOrder();
+ $default_sort = $feed?->defaultSort();
+ } elseif (!empty(self::$current_get['category'])) {
+ $id = self::$current_get['category'];
+ // We most likely already have the category object in cache
+ $category = FreshRSS_Context::categories()[$id] ?? null;
+ if ($category === null) {
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $category = $categoryDAO->searchById($id);
+ }
+ $default_order = $category?->defaultOrder();
+ $default_sort = $category?->defaultSort();
+ }
+ }
+ $order = Minz_Request::paramString('order', plaintext: true) ?: $default_order ?: FreshRSS_Context::userConf()->sort_order;
self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
- $sort = Minz_Request::paramString('sort', plaintext: true) ?: FreshRSS_Context::userConf()->sort;
- self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+ $sort = Minz_Request::paramString('sort', plaintext: true) ?: $default_sort ?: FreshRSS_Context::userConf()->sort;
+ self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'lastUserModified', 'length', 'link', 'title', 'rand'], true) ? $sort : 'id';
+
+ if (in_array(self::$sort, ['c.name', 'f.name'], true)) {
+ self::$secondary_sort = FreshRSS_Context::userConf()->secondary_sort;
+ self::$secondary_sort_order = FreshRSS_Context::userConf()->secondary_sort_order;
+ if ($order !== ($default_order ?: FreshRSS_Context::userConf()->sort_order)) {
+ // User swapped order so swap secondary order as well
+ self::$secondary_sort_order = self::$secondary_sort_order === 'DESC' ? 'ASC' : 'DESC';
+ }
+ }
+
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(
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index d247b84b7..024aef4e8 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -1270,15 +1270,18 @@ SQL;
/**
* @param numeric-string $id_min
* @param numeric-string $id_max
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+ * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
* @param 'ASC'|'DESC' $order
* @param numeric-string $continuation_id
* @param list<string|int> $continuation_values
+ * @param 'id'|'date'|'link'|'title' $secondary_sort
+ * @param 'ASC'|'DESC' $secondary_sort_order
* @return array{0:list<int|string>,1:string}
*/
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', array $continuation_values = []): array {
+ string $continuation_id = '0', array $continuation_values = [],
+ string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): array {
$search = ' ';
$values = [];
if ($state & FreshRSS_Entry::STATE_ANDS) {
@@ -1338,29 +1341,53 @@ SQL;
$values[] = $id_min;
}
- if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified', 'length'], true)) {
+ if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'lastUserModified', 'length', 'link', 'title'], true)) {
$sign = $order === 'ASC' ? '>' : '<';
+ $sign2 = $secondary_sort_order === 'ASC' ? '>' : '<';
$orderBy = match ($sort) {
'c.name' => 'c.name',
+ 'date' => $alias . 'date',
'f.name' => 'f.name',
'lastUserModified' => $alias . '`lastUserModified`',
'length' => 'LENGTH(' . $alias . (static::isCompressed() ? 'content_bin' : 'content') . ')',
- default => $alias . $sort,
+ 'link' => $alias . 'link',
+ 'title' => $alias . 'title',
+ };
+ $orderBy2 = match ($secondary_sort) {
+ 'id' => $alias . 'id',
+ 'date' => $alias . 'date',
+ 'link' => $alias . 'link',
+ 'title' => $alias . 'title',
};
// Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128)
if ($sort === 'c.name') {
- // Includes a secondary sort by feed name
- $search .= "AND ((c.name {$sign} ?) OR (c.name = ? AND f.name {$sign} ?) OR (c.name = ? AND f.name = ? AND {$alias}id {$sign}= ?)) ";
- $values[] = $continuation_values[0];
- $values[] = $continuation_values[0];
- $values[] = $continuation_values[1];
- $values[] = $continuation_values[0];
- $values[] = $continuation_values[1];
+ // Includes the feed-name sort and a user secondary sort
+ $search .= "AND ((c.name {$sign} ?) OR (c.name = ? AND f.name {$sign} ?) OR (c.name = ? AND f.name = ? AND {$orderBy2} {$sign2}= ?) " .
+ "OR (c.name = ? AND f.name = ? AND {$orderBy2} = ? AND {$alias}id {$sign}= ?)) ";
+ $values[] = $continuation_values[0]; // c.name (primary sort)
+ $values[] = $continuation_values[0]; // c.name (primary sort)
+ $values[] = $continuation_values[1]; // f.name (internal secondary sort)
+ $values[] = $continuation_values[0]; // c.name (primary sort)
+ $values[] = $continuation_values[1]; // f.name (internal secondary sort)
+ $values[] = $continuation_values[2]; // secondary sort
+ $values[] = $continuation_values[0]; // c.name (primary sort)
+ $values[] = $continuation_values[1]; // f.name (internal secondary sort)
+ $values[] = $continuation_values[2]; // secondary sort
+ $values[] = $continuation_id;
+ } elseif ($sort === 'f.name') {
+ // Includes the user secondary sort
+ $search .= "AND ((f.name {$sign} ?) OR (f.name = ? AND {$orderBy2} {$sign2} ?) " .
+ "OR (f.name = ? AND {$orderBy2} = ? AND {$alias}id {$sign}= ?)) ";
+ $values[] = $continuation_values[0]; // f.name (primary sort)
+ $values[] = $continuation_values[0]; // f.name (primary sort)
+ $values[] = $continuation_values[1]; // secondary sort
+ $values[] = $continuation_values[0]; // f.name (primary sort)
+ $values[] = $continuation_values[1]; // secondary sort
$values[] = $continuation_id;
} else {
$search .= "AND ({$orderBy} {$sign} ? OR ({$orderBy} = ? AND {$alias}id {$sign}= ?)) ";
- $values[] = $continuation_values[0];
- $values[] = $continuation_values[0];
+ $values[] = $continuation_values[0]; // primary sort
+ $values[] = $continuation_values[0]; // primary sort
$values[] = $continuation_id;
}
}
@@ -1382,16 +1409,19 @@ SQL;
* @param int $id category/feed/tag ID
* @param numeric-string $id_min
* @param numeric-string $id_max
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+ * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
* @param 'ASC'|'DESC' $order
* @param numeric-string $continuation_id
* @param list<string|int> $continuation_values
+ * @param 'id'|'date'|'link'|'title' $secondary_sort
+ * @param 'ASC'|'DESC' $secondary_sort_order
* @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, ?FreshRSS_BooleanSearch $filters = null,
string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
- string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): array {
+ string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+ string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): array {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
}
@@ -1441,17 +1471,27 @@ SQL;
}
$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
- $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+ $order2 = in_array($secondary_sort_order, ['ASC', 'DESC'], true) ? $secondary_sort_order : 'DESC';
$orderBy = match ($sort) {
+ 'id' => 'e.id',
'c.name' => 'c.name',
+ 'date' => 'e.date',
'f.name' => 'f.name',
'lastUserModified' => 'e.`lastUserModified`',
'length' => 'LENGTH(e.' . (static::isCompressed() ? 'content_bin' : 'content') . ')',
+ 'link' => 'e.link',
+ 'title' => 'e.title',
'rand' => static::sqlRandom(),
- default => 'e.' . $sort,
+ };
+ $orderBy2 = match ($secondary_sort) {
+ 'id' => 'e.id',
+ 'date' => 'e.date',
+ 'link' => 'e.link',
+ 'title' => 'e.title',
};
[$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_values: $continuation_values);
+ sort: $sort, order: $order, continuation_id: $continuation_id, continuation_values: $continuation_values,
+ secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
// Help MySQL/MariaDB's optimizer with the query plan:
$useEntryIndex = ($this->pdo->dbType() === 'mysql' && // Only relevant for MySQL/MariaDB,
@@ -1470,7 +1510,8 @@ SQL;
. 'WHERE ' . $where
. $search
. 'ORDER BY ' . $orderBy . ' ' . $order
- . ($sort === 'c.name' ? ', f.name ' . $order : '') // Secondary sort
+ . ($sort === 'c.name' ? ', f.name ' . $order : '') // Internal secondary sort
+ . (in_array($sort, ['c.name', 'f.name'], true) ? ', ' . $orderBy2 . ' ' . $order2 : '') // User secondary sort
. ($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 : '')
@@ -1482,28 +1523,41 @@ SQL;
* @param int $id category/feed/tag ID
* @param numeric-string $id_min
* @param numeric-string $id_max
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+ * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
* @param 'ASC'|'DESC' $order
* @param numeric-string $continuation_id
* @param list<string|int> $continuation_values
+ * @param 'id'|'date'|'link'|'title' $secondary_sort
+ * @param 'ASC'|'DESC' $secondary_sort_order
* @throws FreshRSS_EntriesGetter_Exception
*/
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', array $continuation_values = [], int $limit = 1, int $offset = 0): PDOStatement|false {
+ string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+ string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): PDOStatement|false {
$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
- $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+ $secondary_sort_order = in_array($secondary_sort_order, ['ASC', 'DESC'], true) ? $secondary_sort_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_values: $continuation_values, limit: $limit, offset: $offset);
+ continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+ secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
$orderBy = match ($sort) {
+ 'id' => 'e0.id',
'c.name' => 'c0.name',
+ 'date' => 'e0.date',
'f.name' => 'f0.name',
'lastUserModified' => 'e0.`lastUserModified`',
'length' => 'LENGTH(e0.' . (static::isCompressed() ? 'content_bin' : 'content') . ')',
+ 'link' => 'e0.link',
+ 'title' => 'e0.title',
'rand' => static::sqlRandom(),
- default => 'e0.' . $sort,
+ };
+ $orderBy2 = match ($secondary_sort) {
+ 'id' => 'e0.id',
+ 'date' => 'e0.date',
+ 'link' => 'e0.link',
+ 'title' => 'e0.title',
};
$content = static::isCompressed() ? 'UNCOMPRESS(e0.content_bin) AS content' : 'e0.content';
$hash = static::sqlHexEncode('e0.hash');
@@ -1520,7 +1574,10 @@ SQL;
}
$sql .= ' ORDER BY ' . $orderBy . ' ' . $order;
if ($sort === 'c.name') {
- $sql .= ', f0.name ' . $order; // Secondary sort
+ $sql .= ', f0.name ' . $order; // Internal secondary sort
+ }
+ if (in_array($sort, ['c.name', 'f.name'], true)) {
+ $sql .= ', ' . $orderBy2 . ' ' . $secondary_sort_order; // User secondary sort
}
if ($sort !== 'id') {
// For keyset pagination
@@ -1537,6 +1594,7 @@ SQL;
continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+ Minz_Log::error('SQL error ' . __METHOD__ . json_encode($sql));
return false;
}
}
@@ -1546,18 +1604,22 @@ SQL;
* @param int $id category/feed/tag ID
* @param numeric-string $id_min
* @param numeric-string $id_max
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+ * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
* @param 'ASC'|'DESC' $order
* @param numeric-string $continuation_id
* @param list<string|int> $continuation_values
+ * @param 'id'|'date'|'link'|'title' $secondary_sort
+ * @param 'ASC'|'DESC' $secondary_sort_order
* @return Traversable<FreshRSS_Entry>
* @throws FreshRSS_EntriesGetter_Exception
*/
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', array $continuation_values = [], int $limit = 1, int $offset = 0): Traversable {
+ string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+ string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): 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_values: $continuation_values, limit: $limit, offset: $offset);
+ continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+ secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
if ($stm !== false) {
while (is_array($row = $stm->fetch(PDO::FETCH_ASSOC))) {
/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
@@ -1618,14 +1680,18 @@ SQL;
* @param 'ASC'|'DESC' $order
* @param numeric-string $continuation_id
* @param list<string|int> $continuation_values
+ * @param 'id'|'date'|'link'|'title' $secondary_sort
+ * @param 'ASC'|'DESC' $secondary_sort_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, ?FreshRSS_BooleanSearch $filters = null,
string $id_min = '0', string $id_max = '0', string $order = 'DESC',
- string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): ?array {
+ string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+ string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): ?array {
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, order: $order,
- continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
+ continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+ secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values)) {
/** @var list<int|numeric-string> $res */
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 6a995fef0..acbe11fe9 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -537,6 +537,13 @@ class FreshRSS_Feed extends Minz_Model {
$this->nbEntries = $value;
}
+ public function defaultSort(): ?string {
+ return $this->attributeString('defaultSort');
+ }
+ public function defaultOrder(): ?string {
+ return $this->attributeString('defaultOrder');
+ }
+
/**
* @throws Minz_FileNotExistException
* @throws FreshRSS_Feed_Exception
diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php
index d12316141..d6a0be027 100644
--- a/app/Models/UserConfiguration.php
+++ b/app/Models/UserConfiguration.php
@@ -56,7 +56,9 @@ declare(strict_types=1);
* @property bool $show_nav_buttons
* @property 'big'|'small'|'none' $mark_read_button
* @property 'ASC'|'DESC' $sort_order
- * @property 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'length' $sort
+ * @property 'id'|'c.name'|'date'|'f.name'|'length'|'link'|'rand'|'title' $sort
+ * @property 'ASC'|'DESC' $secondary_sort_order
+ * @property 'id'|'date'|'link'|'title' $secondary_sort
* @property array<int,array<string,string>> $sharing
* @property array<string,string> $shortcuts
* @property bool $sides_close_article