Compare commits

...

17 Commits

Author SHA1 Message Date
74cbfd945b Merge pull request #8494 from wallabag/release/2.6.14
Prepare 2.6.14
2025-10-07 10:05:47 +02:00
de107deed2 Prepare 2.6.14 2025-10-07 09:49:06 +02:00
7108eea2da Merge pull request #8488 from wallabag/fix/doc-urls-api 2025-10-06 06:05:45 +02:00
7aadb8eaa8 Merge pull request #8332 from andreadecorte/fix-reading-time-computation 2025-10-06 06:05:31 +02:00
f2a3968a2f Merge pull request #8489 from wallabag/fix/bump-deps 2025-10-06 06:04:56 +02:00
19c0b9c800 Bump deps (mostly for siteconfig) 2025-10-02 12:19:53 +02:00
1a69855090 Fix urls parameter when sending many urls to be stored using the API 2025-10-02 10:23:14 +02:00
797d48905c Extract DEFAULT_WORDS_PER_MINUTE in Utils
This is done In order to avoid having magic numbers in the code
2025-09-07 22:32:55 +02:00
d24b703315 Extract user reading time logic in a single place
Extract user reading time computation in a single place under Entry to avoid duplication.

This will also fix reading time computation for short entries.

RuleTagger logic is slightly changed as we will round when filtering, which will be
consistent with frontend display.
2025-09-07 22:27:08 +02:00
1eca3cf327 Merge pull request #8440 from wallabag/fix-docker-base-image
Fix docker base image
2025-08-25 08:55:19 +02:00
91384c531d Fix docker base image 2025-08-24 23:47:18 +02:00
a86b16d679 Merge pull request #8435 from wallabag/update-dependencies
Update dependencies
2025-08-20 15:11:25 +02:00
2bfc3fd852 Update dependencies 2025-08-20 13:04:24 +02:00
653b198f8e Merge pull request #8346 from skn/2.6
Add annotations filter to entries API endpoint
2025-07-11 15:08:08 +02:00
5c5b20c83b Add annotations filter to entries API endpoint
Implement a new filter parameter 'annotations' for the GET /api/entries endpoint
that allows filtering entries based on whether they have annotations. When
annotations=1, only entries with one or more annotations are returned. When
annotations=0, only entries without annotations are returned. This feature
enables users to easily find annotated content through the API.
2025-07-02 22:24:29 +04:00
d00fe83366 Merge pull request #8267 from wallabag/fix/2.6-deprecation
Fix deprecation
2025-06-05 16:32:29 +02:00
f0d3db70c0 Fix deprecation
See https://php.watch/versions/8.2/$%7Bvar%7D-string-interpolation-deprecated
2025-06-05 15:54:51 +02:00
14 changed files with 555 additions and 401 deletions

View File

@ -1,28 +1,48 @@
# Changelog
## [2.6.14](https://github.com/wallabag/wallabag/tree/2.6.14)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.13...2.6.14)
### Improvements
* Add annotations filter to entries API endpoint by @skn in https://github.com/wallabag/wallabag/pull/8346
### Fixes
* Fix reading time computation for short entries by @andreadecorte in https://github.com/wallabag/wallabag/pull/8332
* Fix `urls` parameter when sending many urls to be stored using the API by @j0k3r in https://github.com/wallabag/wallabag/pull/8488
* Fix docker base image by @yguedidi in https://github.com/wallabag/wallabag/pull/8440
### Technical stuff
* Change version in wallabag.yml by @nicosomb in https://github.com/wallabag/wallabag/pull/8251
* Fix deprecation by @j0k3r in https://github.com/wallabag/wallabag/pull/8267
* Update dependencies by @yguedidi in https://github.com/wallabag/wallabag/pull/8435
* Bump deps (mostly for siteconfig) by @j0k3r in https://github.com/wallabag/wallabag/pull/8489
## [2.6.13](https://github.com/wallabag/wallabag/tree/2.6.13)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.12...2.6.13)
### Improvements
* Add support of Pocket CSV import by @kdecherf and @nicosomb in [https://github.com/wallabag/wallabag/pull/8240](https://github.com/wallabag/wallabag/pull/8240)
* Backport Pocket and Shaarli HTML imports from master by @nicosomb in [https://github.com/wallabag/wallabag/pull/8193](https://github.com/wallabag/wallabag/pull/8193)
* Add support of Pocket CSV import by @kdecherf and @nicosomb in https://github.com/wallabag/wallabag/pull/8240
* Backport Pocket and Shaarli HTML imports from master by @nicosomb in https://github.com/wallabag/wallabag/pull/8193
### Fixes
* Avoid non-validated OTP to be enabled #8139 by @j0k3r in [https://github.com/wallabag/wallabag/pull/8139](https://github.com/wallabag/wallabag/pull/8139)
* Avoid non-validated OTP to be enabled #8139 by @j0k3r in https://github.com/wallabag/wallabag/pull/8139
### Technical stuff
* Update j0k3r/php-readability:1.2.13 to fix regression (about latin1 instead of UTF-8 used for entries) by @nicosomb [https://github.com/wallabag/wallabag/pull/8194](https://github.com/wallabag/wallabag/pull/8194)
* Update j0k3r/php-readability:1.2.13 to fix regression (about latin1 instead of UTF-8 used for entries) by @nicosomb https://github.com/wallabag/wallabag/pull/8194
## [2.6.12](https://github.com/wallabag/wallabag/tree/2.6.12)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.11...2.6.12)
### Technical stuff
* Fix changelog by @yguedidi in [https://github.com/wallabag/wallabag/pull/8135](https://github.com/wallabag/wallabag/pull/8135)
* Update dependencies by @yguedidi in [https://github.com/wallabag/wallabag/pull/8136](https://github.com/wallabag/wallabag/pull/8136)
* Fix changelog by @yguedidi in https://github.com/wallabag/wallabag/pull/8135
* Update dependencies by @yguedidi in https://github.com/wallabag/wallabag/pull/8136
## [2.6.11](https://github.com/wallabag/wallabag/tree/2.6.11)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.10...2.6.11)
@ -32,14 +52,14 @@
### Fixes
* Fix redirection after action in search results by @nicosomb in [https://github.com/wallabag/wallabag/pull/7827](https://github.com/wallabag/wallabag/pull/7827)
* Fix title tag filter by @nicosomb in [https://github.com/wallabag/wallabag/pull/7846](https://github.com/wallabag/wallabag/pull/7846)
* Change NB_ELEMENTS in pocket importer to 30 by @j0k3r in [https://github.com/wallabag/wallabag/pull/7993](https://github.com/wallabag/wallabag/pull/7993)
* Fix entries counter for annotated entries in the menu by @j0k3r in [https://github.com/wallabag/wallabag/pull/7999](https://github.com/wallabag/wallabag/pull/7999)
* Fix redirection after action in search results by @nicosomb in https://github.com/wallabag/wallabag/pull/7827
* Fix title tag filter by @nicosomb in https://github.com/wallabag/wallabag/pull/7846
* Change NB_ELEMENTS in pocket importer to 30 by @j0k3r in https://github.com/wallabag/wallabag/pull/7993
* Fix entries counter for annotated entries in the menu by @j0k3r in https://github.com/wallabag/wallabag/pull/7999
### Technical stuff
* Prepare 2.6.11 release by @yguedidi in [https://github.com/wallabag/wallabag/pull/8133](https://github.com/wallabag/wallabag/pull/8133)
* Prepare 2.6.11 release by @yguedidi in https://github.com/wallabag/wallabag/pull/8133
## [2.6.10](https://github.com/wallabag/wallabag/tree/2.6.10)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.9...2.6.10)

View File

@ -27,12 +27,12 @@ class Version20170511211659 extends WallabagMigration
$this->addSql(<<<EOD
CREATE TEMPORARY TABLE __temp__wallabag_annotation AS
SELECT id, user_id, entry_id, text, created_at, updated_at, quote, ranges
FROM ${annotationTableName}
FROM {$annotationTableName}
EOD
);
$this->addSql('DROP TABLE ' . $annotationTableName);
$this->addSql(<<<EOD
CREATE TABLE ${annotationTableName}
CREATE TABLE {$annotationTableName}
(
id INTEGER PRIMARY KEY NOT NULL,
user_id INTEGER DEFAULT NULL,
@ -42,16 +42,16 @@ CREATE TABLE ${annotationTableName}
updated_at DATETIME NOT NULL,
quote CLOB NOT NULL,
ranges CLOB NOT NULL,
CONSTRAINT FK_A7AED006A76ED395 FOREIGN KEY (user_id) REFERENCES ${userTableName} (id),
CONSTRAINT FK_A7AED006BA364942 FOREIGN KEY (entry_id) REFERENCES ${entryTableName} (id) ON DELETE CASCADE
CONSTRAINT FK_A7AED006A76ED395 FOREIGN KEY (user_id) REFERENCES {$userTableName} (id),
CONSTRAINT FK_A7AED006BA364942 FOREIGN KEY (entry_id) REFERENCES {$entryTableName} (id) ON DELETE CASCADE
);
CREATE INDEX IDX_A7AED006A76ED395 ON ${annotationTableName} (user_id);
CREATE INDEX IDX_A7AED006BA364942 ON ${annotationTableName} (entry_id);
CREATE INDEX IDX_A7AED006A76ED395 ON {$annotationTableName} (user_id);
CREATE INDEX IDX_A7AED006BA364942 ON {$annotationTableName} (entry_id);
EOD
);
$this->addSql(<<<EOD
INSERT INTO ${annotationTableName} (id, user_id, entry_id, text, created_at, updated_at, quote, ranges)
INSERT INTO {$annotationTableName} (id, user_id, entry_id, text, created_at, updated_at, quote, ranges)
SELECT id, user_id, entry_id, text, created_at, updated_at, quote, ranges
FROM __temp__wallabag_annotation;
EOD

View File

@ -1,5 +1,5 @@
wallabag_core:
version: 2.6.14-dev
version: 2.6.14
paypal_url: "https://liberapay.com/wallabag/donate"
languages:
en: 'English'

753
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
FROM php:8.1-fpm AS rootless
FROM php:8.1-fpm-bookworm AS rootless
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_VERSION=20

View File

@ -269,6 +269,16 @@ class EntryRestController extends WallabagRestController
* example="example.com",
* )
* ),
* @OA\Parameter(
* name="annotations",
* in="query",
* description="filter by entries with annotations. Use 1 for entries with annotations, 0 for entries without annotations. All entries by default",
* required=false,
* @OA\Schema(
* type="integer",
* enum={"1", "0"}
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
@ -294,6 +304,7 @@ class EntryRestController extends WallabagRestController
$since = $request->query->get('since', 0);
$detail = strtolower($request->query->get('detail', 'full'));
$domainName = (null === $request->query->get('domain_name')) ? '' : (string) $request->query->get('domain_name');
$hasAnnotations = (null === $request->query->get('annotations')) ? null : (bool) $request->query->get('annotations');
try {
/** @var Pagerfanta $pager */
@ -307,7 +318,8 @@ class EntryRestController extends WallabagRestController
$since,
$tags,
$detail,
$domainName
$domainName,
$hasAnnotations
);
} catch (\Exception $e) {
throw new BadRequestHttpException($e->getMessage());
@ -332,6 +344,7 @@ class EntryRestController extends WallabagRestController
'tags' => $tags,
'since' => $since,
'detail' => $detail,
'annotations' => $hasAnnotations,
],
true
)
@ -489,7 +502,7 @@ class EntryRestController extends WallabagRestController
* @OA\Parameter(
* name="urls",
* in="query",
* description="Urls (as an array) to create. A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]",
* description="Urls (as an array) to create. A JSON array of urls ['http://...', 'http://...']",
* required=true,
* @OA\Schema(type="string")
* ),

View File

@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraints as Assert;
use Wallabag\AnnotationBundle\Entity\Annotation;
use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
use Wallabag\CoreBundle\Helper\UrlHasher;
use Wallabag\CoreBundle\Tools\Utils;
use Wallabag\UserBundle\Entity\User;
/**
@ -658,6 +659,14 @@ class Entry
$this->readingTime = $readingTime;
}
/**
* @return float
*/
public function getUserReadingTime()
{
return round($this->readingTime / $this->getUser()->getConfig()->getReadingSpeed() * Utils::DEFAULT_WORDS_PER_MINUTE);
}
/**
* @return string
*/

View File

@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Tools\Utils;
use Wallabag\UserBundle\Entity\User;
class EntryFilterType extends AbstractType
@ -57,8 +58,8 @@ class EntryFilterType extends AbstractType
return;
}
$min = (int) ($lower * $user->getConfig()->getReadingSpeed() / 200);
$max = (int) ($upper * $user->getConfig()->getReadingSpeed() / 200);
$min = (int) ($lower * $user->getConfig()->getReadingSpeed() / Utils::DEFAULT_WORDS_PER_MINUTE);
$max = (int) ($upper * $user->getConfig()->getReadingSpeed() / Utils::DEFAULT_WORDS_PER_MINUTE);
if (null === $lower && null !== $upper) {
// only lower value is defined: query all entries with reading LOWER THAN this value

View File

@ -210,7 +210,7 @@ class EntriesExport
$publishedDate = $entry->getPublishedAt()->format('Y-m-d');
}
$readingTime = round($entry->getReadingTime() / $user->getConfig()->getReadingSpeed() * 200);
$readingTime = $entry->getUserReadingTime();
$titlepage = $content_start .
'<h1>' . $entry->getTitle() . '</h1>' .
@ -331,7 +331,7 @@ class EntriesExport
$authors = implode(',', $publishedBy);
}
$readingTime = $entry->getReadingTime() / $user->getConfig()->getReadingSpeed() * 200;
$readingTime = $entry->getUserReadingTime();
$pdf->addPage();
$html = '<h1>' . $entry->getTitle() . '</h1>' .

View File

@ -133,7 +133,7 @@ class RuleBasedTagger
private function fixEntry(Entry $entry)
{
$clonedEntry = clone $entry;
$clonedEntry->setReadingTime($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200);
$clonedEntry->setReadingTime($entry->getUserReadingTime());
return $clonedEntry;
}

View File

@ -266,14 +266,15 @@ class EntryRepository extends ServiceEntityRepository
* @param string $order
* @param int $since
* @param string $tags
* @param string $detail 'metadata' or 'full'. Include content field if 'full'
* @param string $detail 'metadata' or 'full'. Include content field if 'full'
* @param string $domainName
* @param bool $hasAnnotations
*
* @todo Breaking change: replace default detail=full by detail=metadata in a future version
*
* @return Pagerfanta
*/
public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '')
public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '', $hasAnnotations = null)
{
if (!\in_array(strtolower($detail), ['full', 'metadata'], true)) {
throw new \Exception('Detail "' . $detail . '" parameter is wrong, allowed: full or metadata');
@ -332,6 +333,16 @@ class EntryRepository extends ServiceEntityRepository
$qb->andWhere('e.domainName = :domainName')->setParameter('domainName', $domainName);
}
if (null !== $hasAnnotations) {
if ($hasAnnotations) {
$qb->leftJoin('e.annotations', 'a')
->andWhere('a.id IS NOT NULL');
} else {
$qb->leftJoin('e.annotations', 'a')
->andWhere('a.id IS NULL');
}
}
if (!\in_array(strtolower($order), ['asc', 'desc'], true)) {
throw new \Exception('Order "' . $order . '" parameter is wrong, allowed: asc or desc');
}

View File

@ -1,7 +1,7 @@
{% set reading_time = entry.readingTime / app.user.config.readingSpeed * 200 %}
{% set reading_time = entry.userReadingTime %}
<i class="material-icons grey-text">timer</i>
{% if reading_time > 0 %}
<span>{{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': reading_time|round}) }}</span>
<span>{{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': reading_time}) }}</span>
{% else %}
<span>{{ 'entry.list.reading_time_less_one_minute_short'|trans|raw }}</span>
{% endif %}

View File

@ -4,6 +4,8 @@ namespace Wallabag\CoreBundle\Tools;
class Utils
{
public const DEFAULT_WORDS_PER_MINUTE = 200;
/**
* Generate a token used for Feeds.
*
@ -28,6 +30,6 @@ class Utils
*/
public static function getReadingTime($text)
{
return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200);
return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / self::DEFAULT_WORDS_PER_MINUTE);
}
}

View File

@ -190,6 +190,7 @@ class EntryRestControllerTest extends WallabagApiTestCase
'tags' => 'foo',
'since' => 1443274283,
'public' => 0,
'annotations' => 1,
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
@ -217,6 +218,7 @@ class EntryRestControllerTest extends WallabagApiTestCase
$this->assertStringContainsString('tags=foo', $content['_links'][$link]['href']);
$this->assertStringContainsString('since=1443274283', $content['_links'][$link]['href']);
$this->assertStringContainsString('public=0', $content['_links'][$link]['href']);
$this->assertStringContainsString('annotations=1', $content['_links'][$link]['href']);
}
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
@ -1383,4 +1385,85 @@ class EntryRestControllerTest extends WallabagApiTestCase
$this->assertGreaterThan(0, $content['id']);
$this->assertSame('https://www.lemonde.fr/m-perso/article/2017/06/25/antoine-de-caunes-je-veux-avoir-le-droit-de-tatonner_5150728_4497916.html', $content['url']);
}
public function testGetEntriesWithAnnotationsFilter()
{
// Test filter for entries WITH annotations
// From fixtures: entry1, entry2 have annotations (for admin-user), entry3 has annotations (for bob-user)
// entry4, entry5, entry6 don't have annotations
$this->client->request('GET', '/api/entries', [
'annotations' => 1,
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertArrayHasKey('items', $content['_embedded']);
// Check that only entries with annotations are returned
$entriesWithAnnotations = ['http://0.0.0.0/entry1', 'http://0.0.0.0/entry2'];
$entriesWithoutAnnotations = ['http://0.0.0.0/entry4', 'http://0.0.0.0/entry5', 'http://0.0.0.0/entry6'];
foreach ($content['_embedded']['items'] as $item) {
if (\in_array($item['url'], $entriesWithAnnotations, true)) {
$this->assertNotEmpty($item['annotations'], 'Entry with URL ' . $item['url'] . ' should have annotations');
}
$this->assertNotContains($item['url'], $entriesWithoutAnnotations, 'Entry without annotations should NOT be in the results');
}
// Ensure we have at least the entries with annotations for admin-user
$foundUrls = array_column($content['_embedded']['items'], 'url');
$this->assertContains('http://0.0.0.0/entry1', $foundUrls, 'entry1 with annotations should be in the results');
$this->assertContains('http://0.0.0.0/entry2', $foundUrls, 'entry2 with annotations should be in the results');
// Check pagination links contain the filter
$this->assertArrayHasKey('_links', $content);
foreach (['self', 'first', 'last'] as $link) {
$this->assertArrayHasKey('href', $content['_links'][$link]);
$this->assertStringContainsString('annotations=1', $content['_links'][$link]['href']);
}
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
}
public function testGetEntriesWithoutAnnotationsFilter()
{
// Test filter for entries WITHOUT annotations
$this->client->request('GET', '/api/entries', [
'annotations' => 0,
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertArrayHasKey('items', $content['_embedded']);
// Check that only entries without annotations are returned
$entriesWithoutAnnotations = ['http://0.0.0.0/entry4', 'http://0.0.0.0/entry5', 'http://0.0.0.0/entry6'];
$entriesWithAnnotations = ['http://0.0.0.0/entry1', 'http://0.0.0.0/entry2'];
foreach ($content['_embedded']['items'] as $item) {
if (\in_array($item['url'], $entriesWithoutAnnotations, true)) {
$this->assertEmpty($item['annotations'], 'Entry with URL ' . $item['url'] . ' should NOT have annotations');
}
$this->assertNotContains($item['url'], $entriesWithAnnotations, 'Entry with annotations should NOT be in the results');
}
// Ensure we have the entries without annotations for admin-user
$foundUrls = array_column($content['_embedded']['items'], 'url');
$this->assertContains('http://0.0.0.0/entry4', $foundUrls, 'entry4 without annotations should be in the results');
$this->assertContains('http://0.0.0.0/entry5', $foundUrls, 'entry5 without annotations should be in the results');
$this->assertContains('http://0.0.0.0/entry6', $foundUrls, 'entry6 without annotations should be in the results');
// Check pagination links contain the filter
$this->assertArrayHasKey('_links', $content);
foreach (['self', 'first', 'last'] as $link) {
$this->assertArrayHasKey('href', $content['_links'][$link]);
$this->assertStringContainsString('annotations=0', $content['_links'][$link]['href']);
}
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
}
}