Skip to content

IBX-6773: Fixed loading Bookmarks for non-accessible content items#476

Open
vidarl wants to merge 13 commits into4.6from
IBX-6773_Bookmarks_for_non-accessible_contents_cause_exception_4.6
Open

IBX-6773: Fixed loading Bookmarks for non-accessible content items#476
vidarl wants to merge 13 commits into4.6from
IBX-6773_Bookmarks_for_non-accessible_contents_cause_exception_4.6

Conversation

@vidarl
Copy link
Contributor

@vidarl vidarl commented Feb 11, 2025

Question Answer
JIRA issue IBX-6773
Type bug
Target Ibexa version v4.6
BC breaks yes

Related PRs:

Description:

If user bookmark some location which he later looses access too, then the bookmark list in admin-ui fails with an exception.
Simply fixing BookmarkService::loadBookmarks() would be easy. The problem is to implement countUserBookmarks() in persistence layer and having it taking into account user permissions so that BC would be kept.

Talked with Adam on how to solve this without breaking BC and he suggested implementing it using filtering

The Bookmark filter will only work with location filtering (LocationService::find()), not with content (ContentService::find())

This is a port of ezsystems/ezplatform-kernel#408 which was not approved and merge in time before 3.3 went EOL.

For QA:

Read ticket for info on how to reproduce

Documentation:

Documentation for the new filter needs to be made, indeed

@sonarqubecloud
Copy link

@vidarl vidarl requested a review from a team February 12, 2025 12:28
@vidarl vidarl added the Doc needed The changes require some documentation label Feb 12, 2025
@adamwojs adamwojs changed the title IBX:6773 bookmarks for non accessible contents cause exception IBX:6773: Bookmarks for non accessible contents cause exception Feb 23, 2025
@vidarl vidarl force-pushed the IBX-6773_Bookmarks_for_non-accessible_contents_cause_exception_4.6 branch from 9164daa to ec96060 Compare February 23, 2026 15:43
@konradoboza konradoboza requested a review from a team February 24, 2026 08:56
Copy link
Contributor

@konradoboza konradoboza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you make sure none of the deprecated classes are still in use?

@vidarl
Copy link
Contributor Author

vidarl commented Feb 24, 2026

@konradoboza : FYI : Had to add related PR : ibexa/admin-ui#1835

Comment on lines +1993 to +1994
$this->assertEquals($contactUsLocationId, $afterSwap->items[0]->id);
$this->assertEquals($beforeSwap->items[1]->id, $afterSwap->items[1]->id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? It's not clear at first glance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@konradoboza
The testcase initially tests swapping two locations that are both bookmarked.
I modified the test to check behavior when 1 location is bookmarked and the other is not

@alongosz alongosz force-pushed the IBX-6773_Bookmarks_for_non-accessible_contents_cause_exception_4.6 branch from 78b2c3b to 648ac3e Compare February 27, 2026 20:09
@alongosz
Copy link
Member

PR rebased.

Copy link
Contributor

@konradoboza konradoboza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some integration test scenarion within Ibexa\Tests\Integration\Core\Repository\BookmarkServiceTest?

@vidarl
Copy link
Contributor Author

vidarl commented Mar 10, 2026

Did you make sure none of the deprecated classes are still in use?

@konradoboza : Yeah, did a search and found nothing except the use in test:
core/tests/lib/Persistence/Cache/BookmarkHandlerTest.php
core/tests/lib/Persistence/Legacy/Bookmark/Gateway/DoctrineDatabaseTest.php
core/tests/lib/Persistence/Legacy/Bookmark/HandlerTest.php

Should those tests be marked deprecated somehow to ?

@konradoboza
Copy link
Contributor

Not at all. We just change the usage of the method being deprecated from day one. Tests will be adjusted accordingly on the actual removal of the code in question.

Co-authored-by: Konrad Oboza <konrad.oboza@ibexa.co>
@vidarl vidarl requested a review from konradoboza March 10, 2026 09:39
@konradoboza
Copy link
Contributor

#476 (review) please take also this into account.

@vidarl
Copy link
Contributor Author

vidarl commented Mar 10, 2026

Can we have some integration test scenarion within Ibexa\Tests\Integration\Core\Repository\BookmarkServiceTest?

Those integration tests uses BookmarkService. And the BookmarkService is already updated to use the filter instead of the deprecated functions from persistence layer.

But I added also a test for count ( BookmarkServiceTest::testCountBookmarks() ) in 5fd0eb2

Edit: with a fixup : 993e850

@konradoboza

vidarl and others added 2 commits March 10, 2026 14:28
Co-authored-by: Konrad Oboza <konrad.oboza@ibexa.co>
@sonarqubecloud
Copy link

Copy link
Member

@alongosz alongosz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vidarl I'm very sorry for the delayed review.
+1 for the idea of using Filtering for this, but I have concerns about API design and some other minor remarks:

/**
* Loads bookmarks owned by user.
*
* @deprecated The "Handler::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecating it in 6.0 is fine for now, but the annotation format is not correct and most likely it's gonna affect or even crash phpDocumentor ;-)
Please see PHP Conventions: Deprecation standards.

* Supported operators:
* - EQ: matches against a unique user id
*/
final class IsBookmarked extends Criterion implements FilteringCriterion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending criterion means that this would be available not just for filtering but for search too. Hence it would require implementing handlers for all supported search engines.

Filtering was supposed initially to mirror search service, so there are no examples of Filtering Criteria that are not Search Criteria at the same time, but the intention of Filtering was that it can exist as a separate concepts for data which are not supposed to be a part of Search Index but a database.

ATM it's possible to use it with search and get the following fatal error at runtime:

Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException : Intentionally not implemented: No visitor available for: Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked with operator =

If it doesn't rely on Criterion, the error should appear on at least static analysis level, which provides better DX.

Comment on lines +86 to 88
* @deprecated The "BookmarkHandler::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't repeat the same message here. It should be present on the interface only.
In fact, you can drop the entire block because doc inheritance is done by default (it's legacy annotation here).

Comment on lines 127 to +152
@@ -146,6 +148,8 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}

/**
* @deprecated The "DoctrineDatabase::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark here

Comment on lines 61 to +75
@@ -66,6 +71,9 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}
}

/**
* @deprecated The "ExceptionConversion::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too

/**
* @deprecated The "ExceptionConversion::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* @return array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PHPDoc return doesn't provide any extra value. It should be documented as at least array<string, mixed> but on the interface (or abstract) level.

*/
final class IsBookmarked extends Criterion implements FilteringCriterion
{
public function __construct(int $value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

API design remark

Typically you'd want here bool $isBookmarked = true. Semantically IsBookmarked(14) doesn't read well and is rather not expected with the verb Is.

User ID value should be part of a handler (query builder), not a criterion. That being said, I see you provided a case for IsBookmark of user X or user Z. It's a bit out of scope of the usage of this criterion, but if you indeed want such flexibility, user ID should be an optional 2nd argument (by default null, taken from current user in query builder).

);

/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked $criterion */
$value = $criterion->value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As described earlier, this should be part of the logic here, not user input by default. Inject PermissionResolver and get current user ID


$list = new BookmarkList();
$list->totalCount = $result->totalCount;
$list->items = $result->locations;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locations is a magic property, which using of should be avoided due to performance. You should rather do iterator_to_array here.

Comment on lines +39 to +55
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked $criterion */
$value = $criterion->value;

if (\is_array($value)) {
if (!isset($value[0])) {
throw new \InvalidArgumentException('IsBookmarked criterion value must contain userId at index 0.');
}
$value = $value[0];
}

return $queryBuilder->expr()->eq(
'bookmark.user_id',
$queryBuilder->createNamedParameter(
(int)$value,
ParameterType::INTEGER
)
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the API design remark, This could be either:

$userId = $criterion->getUserId() ?? $this->permissionResolver->getCurrentUserReference()->getUserId();

or simply

$userId = $this->permissionResolver->getCurrentUserReference()->getUserId();

PermissionResolver should be injected via constructor.

As for boolean value of is or is not bookmarked, I'd probably use ternary expression based on $criterion->isBookmarked() getter and call neq or eq depending on the value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Doc needed The changes require some documentation Ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants