Add feed visibility filter to unread dates view (#8489)

* Add feed visibility filter to unread dates view

* Date field sanitize
This commit is contained in:
Alexandre Alapetite
2026-02-08 20:42:58 +01:00
committed by GitHub
parent 6b5304b825
commit f17ed2f7c8
5 changed files with 39 additions and 15 deletions

View File

@@ -257,7 +257,8 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
if (!in_array($granularity, ['day', 'month', 'year'], true)) {
$granularity = 'day';
}
$dates = $statsDAO->getMaxUnreadDates($field, $granularity, Minz_Request::paramInt('max') ?: 100);
$dates = $statsDAO->getMaxUnreadDates($field, $granularity, Minz_Request::paramInt('max') ?: 100,
Minz_Request::paramIntNull('min_priority') ?? FreshRSS_Feed::PRIORITY_HIDDEN);
$this->view->unreadDates = $dates;
}
}

View File

@@ -17,7 +17,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
* @param 'day'|'month'|'year' $granularity of the date intervals
*/
protected function sqlDateToIsoGranularity(string $field, int $precision, string $granularity): string {
if (!preg_match('/^[a-zA-Z0-9_]+$/', $field)) {
if (!preg_match('/^[a-zA-Z0-9_.]+$/', $field)) {
throw new InvalidArgumentException('Invalid date field!');
}
$offset = $this->getTimezoneOffset();
@@ -388,19 +388,28 @@ SQL;
* @param 'day'|'month'|'year' $granularity of the date intervals
* @return list<array{'granularity':string,'unread_count':int}>
*/
public function getMaxUnreadDates(string $field, string $granularity, int $max = 100): array {
public function getMaxUnreadDates(string $field, string $granularity, int $max = 100, int $minPriority = FreshRSS_Feed::PRIORITY_HIDDEN): array {
$sql = <<<SQL
SELECT
{$this->sqlDateToIsoGranularity($field, precision: $field === 'id' ? 1000000 : 1, granularity: $granularity)} AS granularity,
COUNT(*) AS unread_count
FROM `_entry`
WHERE is_read = 0
GROUP BY granularity
ORDER BY unread_count DESC, granularity DESC
LIMIT $max;
SQL;
$res = $this->fetchAssoc($sql);
/** @var list<array{granularity:string,unread_count:int}>|null $res */
return is_array($res) ? $res : [];
SELECT
{$this->sqlDateToIsoGranularity('e.' . $field, precision: $field === 'id' ? 1000000 : 1, granularity: $granularity)} AS granularity,
COUNT(*) AS unread_count
FROM `_entry` e
INNER JOIN `_feed` f ON e.id_feed = f.id
WHERE e.is_read = 0 AND f.priority >= :min_priority
GROUP BY granularity
ORDER BY unread_count DESC, granularity DESC
LIMIT :max
SQL;
if (($stm = $this->pdo->prepare($sql)) !== false &&
$stm->bindValue(':min_priority', $minPriority, PDO::PARAM_INT) &&
$stm->bindValue(':max', $max, PDO::PARAM_INT) &&
$stm->execute() && is_array($res = $stm->fetchAll(PDO::FETCH_ASSOC))) {
/** @var list<array{granularity:string,unread_count:int}> $res */
return $res;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return [];
}
}
}

View File

@@ -5,6 +5,9 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
#[\Override]
protected function sqlDateToIsoGranularity(string $field, int $precision, string $granularity): string {
if (!preg_match('/^[a-zA-Z0-9_.]+$/', $field)) {
throw new InvalidArgumentException('Invalid date field!');
}
$offset = $this->getTimezoneOffset();
return match ($granularity) {
'day' => "to_char(to_timestamp(($field / $precision) + $offset), 'YYYY-MM-DD')",

View File

@@ -5,6 +5,9 @@ class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
#[\Override]
protected function sqlDateToIsoGranularity(string $field, int $precision, string $granularity): string {
if (!preg_match('/^[a-zA-Z0-9_.]+$/', $field)) {
throw new InvalidArgumentException('Invalid date field!');
}
$offset = $this->getTimezoneOffset();
return match ($granularity) {
'day' => "strftime('%Y-%m-%d', ($field / $precision) + $offset, 'unixepoch')",

View File

@@ -18,6 +18,14 @@
<option value="month" <?= Minz_Request::paramString('granularity') === 'month' ? 'selected="selected"' : '' ?>><?= _t('gen.period.months') ?></option>
<option value="year" <?= Minz_Request::paramString('granularity') === 'year' ? 'selected="selected"' : '' ?>><?= _t('gen.period.years') ?></option>
</select>
<select name="min_priority" id="min_priority">
<?php $currentPriority = Minz_Request::paramIntNull('min_priority'); ?>
<option value=""><?= _t('sub.feed.priority._') ?></option>
<option value="<?= FreshRSS_Feed::PRIORITY_IMPORTANT ?>" <?= $currentPriority === FreshRSS_Feed::PRIORITY_IMPORTANT ? 'selected="selected"' : '' ?>><?= _t('sub.feed.priority.important') ?></option>
<option value="<?= FreshRSS_Feed::PRIORITY_MAIN_STREAM ?>" <?= $currentPriority === FreshRSS_Feed::PRIORITY_MAIN_STREAM ? 'selected="selected"' : '' ?>><?= _t('sub.feed.priority.main_stream') ?></option>
<option value="<?= FreshRSS_Feed::PRIORITY_CATEGORY ?>" <?= $currentPriority === FreshRSS_Feed::PRIORITY_CATEGORY ? 'selected="selected"' : '' ?>><?= _t('sub.feed.priority.category') ?></option>
<option value="<?= FreshRSS_Feed::PRIORITY_FEED ?>" <?= $currentPriority === FreshRSS_Feed::PRIORITY_FEED ? 'selected="selected"' : '' ?>><?= _t('sub.feed.priority.feed') ?></option>
</select>
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
</form>
<table>