Skip to content

NGSTACK-977 tags visibility #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 59 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0c21b61
NGSTACK-977 add 'isHidden' and 'isInvisible' properties to Tag object…
AntePrkacin May 23, 2025
71034c0
NGSTACK-977 update database schema to include 'is_hidden' and 'is_inv…
AntePrkacin May 23, 2025
981a985
NGSTACK-977 add support for 'isHidden' and 'isInvisible' properties i…
AntePrkacin May 23, 2025
d3dc59e
NGSTACK-977 add support for 'isHidden' and 'isInvisible' fields in tr…
AntePrkacin May 23, 2025
62ba8ad
NGSTACK-977 add 'Hide tag' button and its functionality in admin inte…
AntePrkacin Jun 4, 2025
0ee8a42
NGSTACK-977 add 'Unhide tag' button and its functionality in admin in…
AntePrkacin Jun 5, 2025
033de91
NGSTACK-977 fix bug when creating queries and update translations to …
AntePrkacin Jun 5, 2025
b47e3a5
NGSTACK-977 add functionality for hiding and unhiding selected childr…
AntePrkacin Jun 5, 2025
302837d
NGSTACK-977 throw 404 on frontend if tag is hidden
AntePrkacin Jun 5, 2025
fcabf76
NGSTACK-977 add 'disabled' property to hidden tags and disable adding…
AntePrkacin Jun 5, 2025
402ca4c
NGSTACK-977 add 'hidden' text after hidden tags in eztags content field
AntePrkacin Jun 6, 2025
f12c262
NGSTACK-977 fix php-cs-fixer and phpstan reports
AntePrkacin Jun 6, 2025
9813914
NGSTACK-977 add 'is_hidden' and 'is_invisible' fields to test databas…
AntePrkacin Jun 6, 2025
9987286
NGSTACK-977 fix TagsIntegrationTest by adding 'isHidden' and 'isInvis…
AntePrkacin Jun 6, 2025
c698f26
NGSTACK-977 update phpdocs and remove tag visibility information bein…
AntePrkacin Jun 6, 2025
f8bcf8d
NGSTACK-977 modify gateway to make the hidden tag also invisible
AntePrkacin Jun 6, 2025
e5b2f5f
NGSTACK-977 remove unnecessary cast to int for 'is_hidden' and 'is_in…
AntePrkacin Jun 6, 2025
7e88bdd
NGSTACK-977 add 'hidesynonym' and 'unhidesynonym' policies
AntePrkacin Jun 6, 2025
dece52c
NGSTACK-977 add translation domain and make tag visibility info trans…
AntePrkacin Jun 6, 2025
543afc4
NGSTACK-977 change all 'unhide' text to 'reveal' regarding the tags v…
AntePrkacin Jun 6, 2025
8b20a72
NGSTACK-977 make descendant tags who have one hidden parent stay invi…
AntePrkacin Jun 9, 2025
54ec1e9
NGSTACK-977 fix logic for revealing a hidden tag
AntePrkacin Jun 9, 2025
a61028b
NGSTACK-977 set 'is_hidden' and 'is_invisible' fields correctly when …
AntePrkacin Jun 9, 2025
45b2f03
NGSTACK-977 update logic for revealing a tag to handle both ancestor …
AntePrkacin Jun 9, 2025
e50dfdb
NGSTACK-977 fix a test for creating a tag by adding 'is_invisible' pr…
AntePrkacin Jun 9, 2025
69672a5
NGSTACK-977 add events for hiding and revealing a tag and add tests f…
AntePrkacin Jun 9, 2025
086e954
NGSTACK-977 add 'show_hidden' parameter in the configTreeBuilder and …
AntePrkacin Jun 10, 2025
4af3ecd
NGSTACK-977 inject configResolver into SiteAccessAware/TagsService, a…
AntePrkacin Jun 10, 2025
0f8c7dd
NGSTACK-977 add 'show_hidden' argument to necessary functions in the …
AntePrkacin Jun 10, 2025
75858b8
NGSTACK-977 add 'show_hidden' argument to necessary functions in the …
AntePrkacin Jun 10, 2025
d99a9d4
NGSTACK-977 update PHPDoc for hide and reveal methods and add ' (hidd…
AntePrkacin Jun 10, 2025
d09d660
NGSTACK-977 add ' (hidden)' next to tag keyword in the TagTreeData if…
AntePrkacin Jun 10, 2025
a6d7415
NGSTACK-977 update DoctrineDatabaseTest and TagsHandlerTest with test…
AntePrkacin Jun 10, 2025
eed05e2
NGSTACK-977 add 'hide_tag' and 'reveal_tag' data to the TagTree in Tr…
AntePrkacin Jun 11, 2025
37da894
NGSTACK-977 add visibility information for children when listing them…
AntePrkacin Jun 11, 2025
e69b243
NGSTACK-977 not show hidden tags when trying to search for them in ez…
AntePrkacin Jun 11, 2025
514e3d0
NGSTACK-977 update postgresql schema and legacy.yaml file with 'is_hi…
AntePrkacin Jun 11, 2025
81d4007
NGSTACK-977 update PHPDoc for isHidden and isInvisible Tag properties
AntePrkacin Jun 12, 2025
a720391
NGSTACK-977 modify 'tags_reveal' to 'tags_revealed' translation message
AntePrkacin Jun 12, 2025
a69c38c
NGSTACK-977 extract TagTreeText formatting into a separate function a…
AntePrkacin Jun 12, 2025
08ef43d
NGSTACK-977 add upgrade instructions for tag visibility to UPGRADE.md…
AntePrkacin Jun 17, 2025
550f0e3
NGSTACK-977 update hideAction() and revealAction() methods for hiding…
AntePrkacin Jun 23, 2025
4a174cc
NGSTACK-977 separate escape logic from the formatting logic for tag k…
AntePrkacin Jun 23, 2025
6692248
NGSTACK-977 rename 'showHidden' to 'showHiddenTags' parameter in conf…
AntePrkacin Jun 23, 2025
203dcd6
NGSTACK-977 add MySQL and PostgreSQL upgrade scripts for 'is_hidden' …
AntePrkacin Jun 23, 2025
ec62925
NGSTACK-977 fix a call to searchTags() method in FieldController
AntePrkacin Jun 23, 2025
c05ab16
NGSTACK-977 rename MoveTagsType form to MultiselectTagsType form and …
AntePrkacin Jun 24, 2025
07866bc
NGSTACK-977 update hideTagsAction and revealTagsAction methods in Tag…
AntePrkacin Jun 24, 2025
535890e
NGSTACK-977 add 'autocomplete_provide_hidden_tags' parameter to the c…
AntePrkacin Jun 24, 2025
96279be
NGSTACK-977 modify autoCompleteAction() method in FieldController to …
AntePrkacin Jun 24, 2025
9a00e4e
NGSTACK-977 make autocomplete suggestions not show invisible tags whe…
AntePrkacin Jun 26, 2025
cf15dfc
NGSTACK-977 add data about tag visibility when searching for tags in …
AntePrkacin Jun 26, 2025
fd4e11f
NGSTACK-977 fix tag converting to synonym process and merging process…
AntePrkacin Jun 26, 2025
c81ccef
NGSTACK-977 remove dump statements from 'hide' and 'reveal' twig temp…
AntePrkacin Jun 26, 2025
45cf8e6
NGSTACK-977 update 'is_hidden' check to 'is_invisible' check in metho…
AntePrkacin Jun 26, 2025
50a4c29
NGSTACK-977 fix revealing a hidden synonym by checking the 'main_tag_…
AntePrkacin Jun 26, 2025
40605b9
NGSTACK-977 update testConvertToSynonym to check 'is_invisible' property
AntePrkacin Jun 26, 2025
88fc80e
NGSTACK-977 add some missing translations for french langauge
AntePrkacin Jun 26, 2025
71a9102
NGSTACK-977 change policies to only have one for hiding and revealing
AntePrkacin Jun 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bundle/API/Repository/Events/Tags/BeforeHideTagEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Netgen\TagsBundle\API\Repository\Events\Tags;

use Ibexa\Contracts\Core\Repository\Event\BeforeEvent;
use Netgen\TagsBundle\API\Repository\Values\Tags\Tag;

class BeforeHideTagEvent extends BeforeEvent
{
public function __construct(private readonly Tag $tag) {}

public function getTag(): Tag
{
return $this->tag;
}
}
18 changes: 18 additions & 0 deletions bundle/API/Repository/Events/Tags/BeforeRevealTagEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Netgen\TagsBundle\API\Repository\Events\Tags;

use Ibexa\Contracts\Core\Repository\Event\BeforeEvent;
use Netgen\TagsBundle\API\Repository\Values\Tags\Tag;

class BeforeRevealTagEvent extends BeforeEvent
{
public function __construct(private readonly Tag $tag) {}

public function getTag(): Tag
{
return $this->tag;
}
}
18 changes: 18 additions & 0 deletions bundle/API/Repository/Events/Tags/HideTagEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Netgen\TagsBundle\API\Repository\Events\Tags;

use Ibexa\Contracts\Core\Repository\Event\AfterEvent;
use Netgen\TagsBundle\API\Repository\Values\Tags\Tag;

class HideTagEvent extends AfterEvent
{
public function __construct(private readonly Tag $tag) {}

public function getTag(): Tag
{
return $this->tag;
}
}
18 changes: 18 additions & 0 deletions bundle/API/Repository/Events/Tags/RevealTagEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Netgen\TagsBundle\API\Repository\Events\Tags;

use Ibexa\Contracts\Core\Repository\Event\AfterEvent;
use Netgen\TagsBundle\API\Repository\Values\Tags\Tag;

class RevealTagEvent extends AfterEvent
{
public function __construct(private readonly Tag $tag) {}

public function getTag(): Tag
{
return $this->tag;
}
}
34 changes: 27 additions & 7 deletions bundle/API/Repository/TagsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function loadTagByUrl(string $url, array $languages): Tag;
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList
*/
public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList;
public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHidden = null): TagList;
Copy link
Member

@pspanja pspanja Jun 12, 2025

Choose a reason for hiding this comment

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

Possibly we should have a default value for $showHidden here, instead of resolving it in the Persistence implementation. @emodric I'll leave that up to you :)

(Then the same applies for the similar cases below)

Edit: ignore the above, I think the parameter should not be optional in the Persistence layer, we probably need it to be optional here so we can properly handle it in the siteaccess-aware implementation.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @pspanja , however, nullable bool seems weird. Either we show or hide hidden tags, there's no third option for null. So bool $showHidden = false is probably good enough.

Copy link
Member

@pspanja pspanja Jun 18, 2025

Choose a reason for hiding this comment

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

@emodric one question though, if here the default is not null, how do we detect explicitly passed value in the siteaccess-aware layer?

Copy link
Member

Choose a reason for hiding this comment

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

Hm. Good point. Although, do we even have the need for explicit API layer control of this flag? Can we rely only on siteaccess aware config?

Copy link
Member

Choose a reason for hiding this comment

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

I would say we do, this is not only for rendering, it might be used in import for example.

Copy link
Author

@AntePrkacin AntePrkacin Jun 24, 2025

Choose a reason for hiding this comment

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

If $showHidden were not nullable, as mentioned before, you wouldn't need double check here and in other places.

Do I then leave it to be null by default or should I change it?

Copy link
Member

Choose a reason for hiding this comment

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

I think this is okay, but let @pspanja confirm too.

Copy link
Member

Choose a reason for hiding this comment

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

In general I'm OK with it, but I'm not clear about this:

It's just weird design, is all, since API interface presumes that somwhere down below, the value we give to the method might be overriden.

That should definitely not be the case, if the value was passed to the API method call, it must not be overridden somewhere below. That's why in the original comment I suggested making the parameter mandatory in the Persistence layer. It should be controlled from API layer only - either through siteaccess aware layer or by being explicitly given when calling the method.

Copy link
Member

Choose a reason for hiding this comment

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

What I meant was, it looks like API interface is assuming the implementation, or at least one of them.

In terms of API, we either show hidden tags, or don't show hidden tags, there's no third option. And then along comes the third option, that says: "let the implementation decide", which is kinda ambiguous since the user does not know what the end result will be. That's why I'm saying that this API interface is looking weird.

But no matter, we already decided that this is relatively fine, so lets just move on :)

Copy link
Member

Choose a reason for hiding this comment

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

As mentioned f2f, we need to document that null is interpreted true (or false), and then it's kind of OK :)


/**
* Returns the number of children of a tag object.
Expand All @@ -89,7 +89,7 @@ public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit =
*
* @return int
*/
public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true): int;
public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHidden = null): int;

/**
* Loads tags by specified keyword.
Expand All @@ -104,7 +104,7 @@ public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null,
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList
*/
public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): TagList;
public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHidden = null): TagList;

/**
* Returns the number of tags by specified keyword.
Expand All @@ -117,7 +117,7 @@ public function loadTagsByKeyword(string $keyword, string $language, bool $useAl
*
* @return int
*/
public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true): int;
public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true, ?bool $showHidden = null): int;

/**
* Search for tags.
Expand All @@ -132,7 +132,7 @@ public function getTagsByKeywordCount(string $keyword, string $language, bool $u
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\SearchResult
*/
public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult;
public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHidden = null): SearchResult;

/**
* Loads synonyms of a tag object.
Expand All @@ -148,7 +148,7 @@ public function searchTags(string $searchString, string $language, bool $useAlwa
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList
*/
public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList;
public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHidden = null): TagList;

/**
* Returns the number of synonyms of a tag object.
Expand All @@ -162,7 +162,7 @@ public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?arr
*
* @return int
*/
public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true): int;
public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHidden = null): int;

/**
* Loads content related to $tag.
Expand Down Expand Up @@ -286,6 +286,26 @@ public function newSynonymCreateStruct(int $mainTagId, string $mainLanguageCode)
*/
public function newTagUpdateStruct(): TagUpdateStruct;

/**
* Hides $tag.
*
* If $tag is a synonym, only the synonym is hidden.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to hide this tag
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If the specified tag is not found
*/
public function hideTag(Tag $tag): void;

/**
* Reveal $tag.
*
* If $tag is a synonym, only the synonym is revealed.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to reveal this tag
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If the specified tag is not found
*/
public function revealTag(Tag $tag): void;

/**
* Allows tags API execution to be performed with full access sand-boxed.
*
Expand Down
13 changes: 13 additions & 0 deletions bundle/API/Repository/Values/Tags/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* @property-read bool $alwaysAvailable Indicates if the Tag object is shown in the main language if it is not present in an other requested language
* @property-read string $mainLanguageCode The main language code of the Tag object
* @property-read string[] $languageCodes List of languages in this Tag object
* @property-read bool $isHidden Indicates if the Tag object is visible or not
* @property-read bool $isInvisible Indicates if the Tag object is located under another hidden Tag object
*/
final class Tag extends ValueObject
{
Expand Down Expand Up @@ -104,6 +106,17 @@ final class Tag extends ValueObject
*/
protected ?string $prioritizedLanguageCode;

/**
* Indicates that the Tag is hidden.
*/
protected bool $isHidden;

/**
* Indicates that the Tag object is not visible, being either hidden itself,
* or implicitly hidden by parent or ancestor Tag object.
*/
protected bool $isInvisible;

/**
* Construct object optionally with a set of properties.
*
Expand Down
1 change: 1 addition & 0 deletions bundle/Controller/Admin/FieldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function autoCompleteAction(Request $request): JsonResponse
$searchResult = $this->tagsService->searchTags(
$request->query->get('searchString') ?? '',
$request->query->get('locale') ?? '',
showHidden: false,
Copy link
Member

Choose a reason for hiding this comment

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

Should this be controlled through the configuration as well?

Copy link
Author

Choose a reason for hiding this comment

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

I hardcoded here false because the autoCompleteAction() method is used for searching a tag to add it in eztags field. If the showHidden parameter wasn't false here, then the user could add a hidden tag to some content by searching for it when editing a content.

Copy link
Member

Choose a reason for hiding this comment

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

True, but there might be a use case where you prepare the Tags and Content in advance, and just reveal the Tags at some time point. I'll leave it to @emodric.

cc @Ljudevit for info, maybe you have a use case for this?

Copy link
Member

Choose a reason for hiding this comment

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

I would like to agree with @AntePrkacin on this one that we should always avoid outputting hidden tags in the autocomplete actions like searching for tags in tag fields, but there can be some quite silly edge-cases where editor would hide the tag so tags/view page is not visible yet, but they would like to start filling in the content with those hidden tags before revealing the tag all-together.

For sure this configuration parameter must be set to false as default, but for those pesky edge-cases, a parameter is needed.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, parameter is definitely needed.

Also, please do not use named parameters, at least not mixed like this, it just looks wrong :D Either change all of them to use named parameters, or none.

Copy link
Author

Choose a reason for hiding this comment

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

Added config parameter called autocomplete_provide_hidden_tags. Set it to false by default. Didn't use named parameter: 535890e 96279be

Also, just to follow up on this discussion - if some tag is hidden by parent (so it is invisible, but not hidden), should the autocomplete method show this tag or not? Probably not, right?

Copy link
Member

Choose a reason for hiding this comment

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

Hidden and invisible is, for all intents and purposes, basically the same. The difference comes from hiding/unhiding procedure, since you need to care about whether one tag was hidden automatically or manually.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, thanks!
Updated the autocomplete process in searching to not show tags that are invisible (hidden by parent): 9a00e4e

);

$data = $data = $this->filterTags($searchResult->tags, $subTreeLimit, $hideRootTag);
Expand Down
94 changes: 94 additions & 0 deletions bundle/Controller/Admin/TagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,49 @@ public function childrenAction(Request $request, ?Tag $tag = null): Response
);
}

if ($request->request->has('HideTagsAction')) {
return $this->redirectToRoute(
'netgen_tags_admin_tag_hide_tags',
[
'parentId' => $tag?->id ?? 0,
],
);
}

if ($request->request->has('RevealTagsAction')) {
return $this->redirectToRoute(
'netgen_tags_admin_tag_reveal_tags',
[
'parentId' => $tag?->id ?? 0,
],
);
}

return $this->redirect($request->getPathInfo());
}

public function hideAction(Request $request, Tag $tag): Response
{
$this->denyAccessUnlessGranted('ibexa:tags:hide' . ($tag->isSynonym() ? 'synonym' : ''));

$this->tagsService->hideTag($tag);
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to check the submitted button and CSRF token here?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we do. There is an example in deleteTagAction method.

Copy link
Author

Choose a reason for hiding this comment

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

Added CSRF token check to hideAction() method in TagController (550f0e3)


$this->addFlashMessage('success', 'tag_hidden', ['%tagKeyword%' => $tag->keyword]);

return $this->redirectToTag($tag);
}

public function revealAction(Request $request, Tag $tag): Response
{
$this->denyAccessUnlessGranted('ibexa:tags:reveal' . ($tag->isSynonym() ? 'synonym' : ''));

$this->tagsService->revealTag($tag);
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to check the submitted button and CSRF token here?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we do. There is an example in deleteTagAction method.

Copy link
Author

Choose a reason for hiding this comment

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

Added CSRF token check to revealAction() method in TagController (550f0e3)


$this->addFlashMessage('success', 'tag_reveal', ['%tagKeyword%' => $tag->keyword]);

return $this->redirectToTag($tag);
}

/**
* This method is called from a form containing all children tags of a tag.
* It shows a confirmation view.
Expand Down Expand Up @@ -666,6 +706,60 @@ public function deleteTagsAction(Request $request, ?Tag $parentTag = null): Resp
);
}

public function hideTagsAction(Request $request, ?Tag $parentTag = null): Response
Copy link
Member

Choose a reason for hiding this comment

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

Same here with CSRF. What we need to do here is create a Symfony multiselect form for selecting the tags in lieu of moveTagsAction method. Same goes for revealTagsAction.

Copy link
Author

Choose a reason for hiding this comment

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

@emodric OK. Just to confirm that I understood correctly - I will remove the 'Hide selected tags' and 'Reveal selected tags' buttons below the children list - childrenAction() method. Instead, I will put two new buttons (somewhere else) that tell 'Hide kids' or 'Reveal kids'. When either of them is pressed, a multiselect form will show up where the user will be able to select the desired tags they want to hide or reveal.

Copy link
Member

Choose a reason for hiding this comment

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

No no, do it in the same way as it is now, but with Symfony form in lieu of "move tags". Try and see if you can reuse the existing form type. It's now called MoveTagsType, try to reuse it by renaming it to e.g. MultiselectTagsType or similar.

Copy link
Author

Choose a reason for hiding this comment

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

Renamed MoveTagsType form to MultiselectTagsType form and used it when hiding and revealing multiple tags:
c05ab16 07866bc

Copy link
Member

Choose a reason for hiding this comment

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

@AntePrkacin Sounds good 👍

{
$this->denyAccessUnlessGranted('ibexa:tags:hide');

$tagIds = (array) $request->request->get(
'Tags',
$request->hasSession() ? $request->getSession()->get('ngtags_tag_ids') : [],
);

if (count($tagIds) === 0) {
return $this->redirectToTag($parentTag);
}

$tags = [];
foreach ($tagIds as $tagId) {
$tags[] = $this->tagsService->loadTag((int) $tagId);
}

foreach ($tags as $tagObject) {
$this->tagsService->hideTag($tagObject);
}

$this->addFlashMessage('success', 'tags_hidden');

return $this->redirectToTag($parentTag);
}

public function revealTagsAction(Request $request, ?Tag $parentTag = null): Response
{
$this->denyAccessUnlessGranted('ibexa:tags:reveal');

$tagIds = (array) $request->request->get(
'Tags',
$request->hasSession() ? $request->getSession()->get('ngtags_tag_ids') : [],
);

if (count($tagIds) === 0) {
return $this->redirectToTag($parentTag);
}

$tags = [];
foreach ($tagIds as $tagId) {
$tags[] = $this->tagsService->loadTag((int) $tagId);
}

foreach ($tags as $tagObject) {
$this->tagsService->revealTag($tagObject);
}

$this->addFlashMessage('success', 'tags_revealed');

return $this->redirectToTag($parentTag);
}

public function searchTagsAction(Request $request): Response
{
$this->denyAccessUnlessGranted('ibexa:tags:read');
Expand Down
39 changes: 34 additions & 5 deletions bundle/Controller/Admin/TreeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Symfony\Contracts\Translation\TranslatorInterface;

use function htmlspecialchars;
use function mb_strtolower;
use function str_replace;

use const ENT_HTML401;
Expand Down Expand Up @@ -44,6 +45,8 @@ public function __construct(
'merge_tag' => $this->translator->trans('tag.tree.merge_tag', [], 'netgen_tags_admin'),
'convert_tag' => $this->translator->trans('tag.tree.convert_tag', [], 'netgen_tags_admin'),
'add_synonym' => $this->translator->trans('tag.tree.add_synonym', [], 'netgen_tags_admin'),
'hide_tag' => $this->translator->trans('tag.tree.hide_tag', [], 'netgen_tags_admin'),
'reveal_tag' => $this->translator->trans('tag.tree.reveal_tag', [], 'netgen_tags_admin'),
];

$this->treeLinks = [
Expand All @@ -55,6 +58,8 @@ public function __construct(
'merge_tag' => $this->router->generate('netgen_tags_admin_tag_merge', ['tagId' => ':tagId']),
'convert_tag' => $this->router->generate('netgen_tags_admin_tag_convert', ['tagId' => ':tagId']),
'add_synonym' => $this->router->generate('netgen_tags_admin_synonym_add_select', ['mainTagId' => ':mainTagId']),
'hide_tag' => $this->router->generate('netgen_tags_admin_tag_hide', ['tagId' => ':tagId']),
'reveal_tag' => $this->router->generate('netgen_tags_admin_tag_reveal', ['tagId' => ':tagId']),
];
}

Expand Down Expand Up @@ -119,13 +124,13 @@ private function getRootTreeData(): array
*/
private function getTagTreeData(Tag $tag, bool $isRoot = false): array
{
$synonymCount = $this->tagsService->getTagSynonymCount($tag);

return [
'id' => $tag->id,
'parent' => $isRoot ? '#' : $tag->parentTagId,
'text' => $synonymCount > 0 ? $this->escape($tag->keyword) . ' (+' . $synonymCount . ')' : $this->escape($tag->keyword),
'text' => $this->formatTagTreeText($tag),
'children' => $this->tagsService->getTagChildrenCount($tag) > 0,
'hidden' => $tag->isHidden,
'invisible' => $tag->isInvisible,
'a_attr' => [
'href' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['show_tag']),
'rel' => $tag->id,
Expand Down Expand Up @@ -165,13 +170,37 @@ private function getTagTreeData(Tag $tag, bool $isRoot = false): array
'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['convert_tag']),
'text' => $this->treeLabels['convert_tag'],
],
[
'name' => 'hide_tag',
'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['hide_tag']),
'text' => $this->treeLabels['hide_tag'],
],
[
'name' => 'reveal_tag',
'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['reveal_tag']),
'text' => $this->treeLabels['reveal_tag'],
],
],
],
];
}

private function escape(string $string): string
private function formatTagTreeText(Tag $tag): string
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't mix escaping and formating. That is, leave escape method as is, and just reuse is in the new formatTagTreeText method. After all, all of the text should be escaped AFTER formatting it, and not just the starting text.

Copy link
Author

Choose a reason for hiding this comment

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

Separated the escape logic from the formatting logic: 4a174cc

{
return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
$synonymCount = $this->tagsService->getTagSynonymCount($tag);

$text = htmlspecialchars($tag->keyword, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');

if ($tag->isHidden) {
$text .= ' (' . mb_strtolower($this->translator->trans('tag.hidden', [], 'netgen_tags_admin')) . ')';
} elseif ($tag->isInvisible) {
$text .= ' (' . $this->translator->trans('tag.hidden_by_parent', [], 'netgen_tags_admin') . ')';
}

if ($synonymCount > 0) {
$text .= ' (+' . $synonymCount . ')';
}

return $text;
}
}
Loading