From ae3072c6cd735a2125db4e2f2986ce148a6b85e3 Mon Sep 17 00:00:00 2001 From: Manik Khajuria Date: Mon, 9 Feb 2026 22:05:59 +0530 Subject: [PATCH 1/9] Fix-for-controlpannel-Searchabletext --- src/plone/restapi/controlpanels/__init__.py | 43 +++++++++++++++++++ src/plone/restapi/controlpanels/interfaces.py | 7 +++ .../serializer/controlpanels/__init__.py | 1 + 3 files changed, 51 insertions(+) diff --git a/src/plone/restapi/controlpanels/__init__.py b/src/plone/restapi/controlpanels/__init__.py index a19875be22..bd530d600d 100644 --- a/src/plone/restapi/controlpanels/__init__.py +++ b/src/plone/restapi/controlpanels/__init__.py @@ -1,8 +1,15 @@ +from plone.dexterity.interfaces import IDexterityFTI +from plone.restapi.controlpanels.interfaces import IContentRulesControlpanel from plone.restapi.controlpanels.interfaces import IControlpanel +from plone.restapi.controlpanels.interfaces import IDexterityTypesControlpanel from Products.CMFCore.utils import getToolByName +from zope.component import getAllUtilitiesRegisteredFor +from zope.component import queryMultiAdapter from zope.interface import implementer from zope.publisher.interfaces import NotFound +import zope + @implementer(IControlpanel) class RegistryConfigletPanel: @@ -38,6 +45,42 @@ def __init__(self, context, request): self.title = self.configlet["title"] self.group = self._get_group_title() + def get_searchable_text(self): + + text_parts = [] + + if self.title: + text_parts.append(self.title) + + if self.schema is not None: + for name, field in zope.schema.getFields(self.schema).items(): + if field.title: + text_parts.append(field.title) + else: + types_list = [] + + if IDexterityTypesControlpanel.providedBy(self): + ftis = getAllUtilitiesRegisteredFor(IDexterityFTI) + for fti in ftis: + if fti.Title(): + text_parts.append(fti.Title()) + + elif IContentRulesControlpanel.providedBy(self): + cpanel = queryMultiAdapter( + (self.context, self.request), name="rules-controlpanel" + ) + if cpanel: + registered_rules = cpanel.registeredRules() + for rule in registered_rules: + if isinstance(rule, dict): + if rule.get("title"): + text_parts.append(rule["title"]) + + if types_list: + text_parts.extend(types_list) + + return [text for text in text_parts if text] + def add(self, names): raise NotFound(self.context, names, self.request) diff --git a/src/plone/restapi/controlpanels/interfaces.py b/src/plone/restapi/controlpanels/interfaces.py index 61ba1a641d..4950998b23 100644 --- a/src/plone/restapi/controlpanels/interfaces.py +++ b/src/plone/restapi/controlpanels/interfaces.py @@ -25,6 +25,13 @@ def update(names): def delete(names): """Remove controlpanel children by names""" + def get_searchable_text(): + """Return searchable text for this controlpannel. + + Schema-based control panels return text from field titles and descriptions. + Other control panels can return custom text. + """ + class IDexterityTypesControlpanel(IControlpanel): """Dexterity Types Control panel""" diff --git a/src/plone/restapi/serializer/controlpanels/__init__.py b/src/plone/restapi/serializer/controlpanels/__init__.py index 86d377380b..759b193a1c 100644 --- a/src/plone/restapi/serializer/controlpanels/__init__.py +++ b/src/plone/restapi/serializer/controlpanels/__init__.py @@ -34,6 +34,7 @@ def __call__(self): ), "title": self.controlpanel.title, "group": self.controlpanel.group, + "searchable_text": self.controlpanel.get_searchable_text(), } From 5dea08f58b5671de8d6da8657d35f4d1a528e63d Mon Sep 17 00:00:00 2001 From: Manik Khajuria Date: Mon, 9 Feb 2026 22:24:25 +0530 Subject: [PATCH 2/9] Added changelog --- news/1981.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1981.bugfix.rst diff --git a/news/1981.bugfix.rst b/news/1981.bugfix.rst new file mode 100644 index 0000000000..c118457757 --- /dev/null +++ b/news/1981.bugfix.rst @@ -0,0 +1 @@ +Added a single Control Panel API endpoint exposing searchable text for global settings search. Fixes #1981. \ No newline at end of file From d7628bc4f397fa0aa82a18b7c96be58671325b1e Mon Sep 17 00:00:00 2001 From: Manik Khajuria Date: Tue, 10 Feb 2026 22:02:39 +0530 Subject: [PATCH 3/9] control panel classes (in types.py and rules.py) and adding safety --- src/plone/restapi/controlpanels/__init__.py | 30 ++----------------- src/plone/restapi/controlpanels/rules.py | 15 ++++++++++ src/plone/restapi/controlpanels/types.py | 13 ++++++++ .../serializer/controlpanels/__init__.py | 14 +++++++-- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/plone/restapi/controlpanels/__init__.py b/src/plone/restapi/controlpanels/__init__.py index bd530d600d..b519aaba41 100644 --- a/src/plone/restapi/controlpanels/__init__.py +++ b/src/plone/restapi/controlpanels/__init__.py @@ -1,10 +1,5 @@ -from plone.dexterity.interfaces import IDexterityFTI -from plone.restapi.controlpanels.interfaces import IContentRulesControlpanel from plone.restapi.controlpanels.interfaces import IControlpanel -from plone.restapi.controlpanels.interfaces import IDexterityTypesControlpanel from Products.CMFCore.utils import getToolByName -from zope.component import getAllUtilitiesRegisteredFor -from zope.component import queryMultiAdapter from zope.interface import implementer from zope.publisher.interfaces import NotFound @@ -52,32 +47,13 @@ def get_searchable_text(self): if self.title: text_parts.append(self.title) + if self.group: + text_parts.append(self.group) + if self.schema is not None: for name, field in zope.schema.getFields(self.schema).items(): if field.title: text_parts.append(field.title) - else: - types_list = [] - - if IDexterityTypesControlpanel.providedBy(self): - ftis = getAllUtilitiesRegisteredFor(IDexterityFTI) - for fti in ftis: - if fti.Title(): - text_parts.append(fti.Title()) - - elif IContentRulesControlpanel.providedBy(self): - cpanel = queryMultiAdapter( - (self.context, self.request), name="rules-controlpanel" - ) - if cpanel: - registered_rules = cpanel.registeredRules() - for rule in registered_rules: - if isinstance(rule, dict): - if rule.get("title"): - text_parts.append(rule["title"]) - - if types_list: - text_parts.extend(types_list) return [text for text in text_parts if text] diff --git a/src/plone/restapi/controlpanels/rules.py b/src/plone/restapi/controlpanels/rules.py index f39fc2b7dd..ccb4e4a675 100644 --- a/src/plone/restapi/controlpanels/rules.py +++ b/src/plone/restapi/controlpanels/rules.py @@ -25,6 +25,21 @@ class ContentRulesControlpanel(RegistryConfigletPanel): def publishTraverse(self, request, name): return self.context.restrictedTraverse("++rule++" + name) + def get_searchable_text(self): + text_parts = super().get_searchable_text() + + cpanel = queryMultiAdapter( + (self.context, self.request), name="rules-controlpanel" + ) + if cpanel: + registered_rules = cpanel.registeredRules() + for rule in registered_rules: + if isinstance(rule, dict): + if rule.get("title"): + text_parts.append(rule["title"]) + + return text_parts + def add(self, names): data = json_body(self.request) rules = queryMultiAdapter((self.context, self.request), name="+rule") diff --git a/src/plone/restapi/controlpanels/types.py b/src/plone/restapi/controlpanels/types.py index 6773980722..ecdf6ec2f8 100644 --- a/src/plone/restapi/controlpanels/types.py +++ b/src/plone/restapi/controlpanels/types.py @@ -1,3 +1,4 @@ +from plone.dexterity.interfaces import IDexterityFTI from plone.i18n.normalizer import idnormalizer from plone.restapi.controlpanels import RegistryConfigletPanel from plone.restapi.controlpanels.interfaces import IDexterityTypesControlpanel @@ -7,6 +8,7 @@ from plone.restapi.interfaces import ISerializeToJson from zExceptions import BadRequest from zope.component import adapter +from zope.component import getAllUtilitiesRegisteredFor from zope.component import queryMultiAdapter from zope.interface import alsoProvides from zope.interface import implementer @@ -22,6 +24,17 @@ class DexterityTypesControlpanel(RegistryConfigletPanel): configlet_id = "dexterity-types" configlet_category_id = "plone-content" + def get_searchable_text(self): + + text_parts = super().get_searchable_text() + + ftis = getAllUtilitiesRegisteredFor(IDexterityFTI) + for fti in ftis: + if fti.Title(): + text_parts.append(fti.Title()) + + return text_parts + def add(self, names): data = json_body(self.request) diff --git a/src/plone/restapi/serializer/controlpanels/__init__.py b/src/plone/restapi/serializer/controlpanels/__init__.py index 759b193a1c..52d32de5c4 100644 --- a/src/plone/restapi/serializer/controlpanels/__init__.py +++ b/src/plone/restapi/serializer/controlpanels/__init__.py @@ -26,7 +26,7 @@ def __init__(self, controlpanel): self.controlpanel = controlpanel def __call__(self): - return { + result = { "@id": "{}/{}/{}".format( self.controlpanel.context.absolute_url(), SERVICE_ID, @@ -34,9 +34,19 @@ def __call__(self): ), "title": self.controlpanel.title, "group": self.controlpanel.group, - "searchable_text": self.controlpanel.get_searchable_text(), } + if hasattr(self.controlpanel, "get_searchable_text"): + result["searchable_text"] = self.controlpanel.get_searchable_text() + else: + if self.controlpanel.title: + result["searchable_text"] = [ + self.controlpanel.title, + self.controlpanel.description, + ] + + return result + def get_jsonschema_for_controlpanel(controlpanel, context, request, form=None): """Build a complete JSON schema for the given controlpanel.""" From a67aea417c5892f4625159cb24feead480ebdf67 Mon Sep 17 00:00:00 2001 From: Manik-Khajuria-5 Date: Tue, 24 Feb 2026 17:58:39 +0530 Subject: [PATCH 4/9] Added suggested doc changes --- news/{1981.bugfix.rst => 1981.bugfix} | 2 +- src/plone/restapi/controlpanels/interfaces.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename news/{1981.bugfix.rst => 1981.bugfix} (56%) diff --git a/news/1981.bugfix.rst b/news/1981.bugfix similarity index 56% rename from news/1981.bugfix.rst rename to news/1981.bugfix index c118457757..e2621d41d5 100644 --- a/news/1981.bugfix.rst +++ b/news/1981.bugfix @@ -1 +1 @@ -Added a single Control Panel API endpoint exposing searchable text for global settings search. Fixes #1981. \ No newline at end of file +Added a single Control Panel API endpoint exposing searchable text for global settings search. @Manik-Khajuria-5 \ No newline at end of file diff --git a/src/plone/restapi/controlpanels/interfaces.py b/src/plone/restapi/controlpanels/interfaces.py index 4950998b23..e442581004 100644 --- a/src/plone/restapi/controlpanels/interfaces.py +++ b/src/plone/restapi/controlpanels/interfaces.py @@ -26,7 +26,7 @@ def delete(names): """Remove controlpanel children by names""" def get_searchable_text(): - """Return searchable text for this controlpannel. + """Return searchable text for this control pannel. Schema-based control panels return text from field titles and descriptions. Other control panels can return custom text. From 4af87cdedc070b6fc2b563c102a37c26764be885 Mon Sep 17 00:00:00 2001 From: Manik_Khajuria Date: Tue, 24 Feb 2026 18:00:03 +0530 Subject: [PATCH 5/9] Apply suggestion from @davisagli Co-authored-by: David Glick --- src/plone/restapi/controlpanels/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/controlpanels/__init__.py b/src/plone/restapi/controlpanels/__init__.py index b519aaba41..52c8c0e295 100644 --- a/src/plone/restapi/controlpanels/__init__.py +++ b/src/plone/restapi/controlpanels/__init__.py @@ -3,7 +3,7 @@ from zope.interface import implementer from zope.publisher.interfaces import NotFound -import zope +import zope.schema @implementer(IControlpanel) From 1e879243acf2d658127c55c593dc3dd1bcd5ed32 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 25 Feb 2026 16:25:25 -0800 Subject: [PATCH 6/9] Update src/plone/restapi/controlpanels/interfaces.py --- src/plone/restapi/controlpanels/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/controlpanels/interfaces.py b/src/plone/restapi/controlpanels/interfaces.py index e442581004..e35b9096af 100644 --- a/src/plone/restapi/controlpanels/interfaces.py +++ b/src/plone/restapi/controlpanels/interfaces.py @@ -26,7 +26,7 @@ def delete(names): """Remove controlpanel children by names""" def get_searchable_text(): - """Return searchable text for this control pannel. + """Return searchable text for this control panel. Schema-based control panels return text from field titles and descriptions. Other control panels can return custom text. From 4a8fc3ec5f203e07e45e884eee5e55b8ef3546c2 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 25 Feb 2026 16:32:14 -0800 Subject: [PATCH 7/9] Update control panel example response with searchable text (generated by running the tests on Plone 6.2 + Python 3.13) --- .../http-examples/controlpanels_get.resp | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/plone/restapi/tests/http-examples/controlpanels_get.resp b/src/plone/restapi/tests/http-examples/controlpanels_get.resp index f8b7144db6..14124b2a4f 100644 --- a/src/plone/restapi/tests/http-examples/controlpanels_get.resp +++ b/src/plone/restapi/tests/http-examples/controlpanels_get.resp @@ -5,76 +5,251 @@ Content-Type: application/json { "@id": "http://localhost:55001/plone/@controlpanels/date-and-time", "group": "General", + "searchable_text": [ + "Date and Time", + "General", + "Portal default timezone", + "Available timezones", + "First weekday" + ], "title": "Date and Time" }, { "@id": "http://localhost:55001/plone/@controlpanels/language", "group": "General", + "searchable_text": [ + "Language", + "General", + "Site language", + "Available languages", + "Show country-specific language variants", + "Show language flags", + "Always show language selector", + "Use the language of the content item", + "Use language codes in URL path for manual override", + "Use cookie for manual override", + "Authenticated users only", + "Set the language cookie always", + "Use subdomain", + "Use top-level domain", + "Use browser language request negotiation" + ], "title": "Language" }, { "@id": "http://localhost:55001/plone/@controlpanels/mail", "group": "General", + "searchable_text": [ + "Mail", + "General", + "SMTP server", + "SMTP port", + "ESMTP username", + "ESMTP password", + "Site 'From' name", + "Site 'From' address", + "E-mail characterset" + ], "title": "Mail" }, { "@id": "http://localhost:55001/plone/@controlpanels/navigation", "group": "General", + "searchable_text": [ + "Navigation", + "General", + "Navigation depth", + "Automatically generate tabs", + "Generate tabs for items other than folders.", + "Sort tabs on", + "Reversed sort order for tabs.", + "Displayed content types", + "Filter on workflow state", + "Show items normally excluded from navigation if viewing their children.", + "Root", + "Sitemap depth", + "Hide children of these types" + ], "title": "Navigation" }, { "@id": "http://localhost:55001/plone/@controlpanels/search", "group": "General", + "searchable_text": [ + "Search", + "General", + "Enable LiveSearch", + "Select content types which should be excluded from search results", + "Crop the item description in search result listings after a number of characters.", + "Sort on", + "Show images in results", + "Image scale for results" + ], "title": "Search" }, { "@id": "http://localhost:55001/plone/@controlpanels/site", "group": "General", + "searchable_text": [ + "Site", + "General", + "Site title", + "Site Logo", + "MIME type of the site favicon", + "Site Favicon", + "Expose Dublin Core metadata", + "Expose sitemap.xml.gz", + "JavaScript integrations included in head section", + "JavaScript integrations included after the footer", + "Display publication date", + "Icon visibility", + "Thumb visibility", + "No Thumbs in portlets", + "No thumbs in list views", + "No thumbs in summary views", + "No thumbs in table views", + "Thumb scale for portlets", + "Thumb scale for listings", + "Thumb scale for tables", + "Thumb scale for summary view", + "Toolbar position", + "Relative URL for the toolbar logo", + "robots.txt", + "Default page IDs", + "Roles that can add keywords" + ], "title": "Site" }, { "@id": "http://localhost:55001/plone/@controlpanels/socialmedia", "group": "General", + "searchable_text": [ + "Social Media", + "General", + "Share social data", + "Twitter username", + "Facebook App ID", + "Facebook username" + ], "title": "Social Media" }, { "@id": "http://localhost:55001/plone/@controlpanels/content-rules", "group": "Content", + "searchable_text": [ + "Content Rules", + "Content" + ], "title": "Content Rules" }, { "@id": "http://localhost:55001/plone/@controlpanels/dexterity-types", "group": "Content", + "searchable_text": [ + "Content Types", + "Content", + "Plone Site", + "Collection", + "Page", + "Folder", + "Link", + "File", + "Image", + "News Item", + "Event", + "DX Test Document" + ], "title": "Content Types" }, { "@id": "http://localhost:55001/plone/@controlpanels/discussion", "group": "Content", + "searchable_text": [ + "Discussion", + "Content", + "Globally enable comments", + "Enable anonymous comments", + "Enable anonymous email field", + "Enable comment moderation", + "Enable editing of comments", + "Enable deleting own comments", + "Comment text transform", + "Captcha", + "Show commenter image", + "Enable moderator email notification", + "Moderator Email Address", + "Enable user email notification" + ], "title": "Discussion" }, { "@id": "http://localhost:55001/plone/@controlpanels/editing", "group": "Content", + "searchable_text": [ + "Editing", + "Content", + "Available editors", + "Default editor", + "Enable External Editor feature", + "Enable link integrity checks", + "Enable locking for through-the-web edits", + "Limit tags/keywords to the current navigation root" + ], "title": "Editing" }, { "@id": "http://localhost:55001/plone/@controlpanels/imaging", "group": "Content", + "searchable_text": [ + "Image Handling", + "Content", + "Allowed image sizes", + "Scaled image quality", + "High pixel density mode", + "Image quality at 2x", + "Image quality at 3x", + "Picture variants", + "Enable image captioning" + ], "title": "Image Handling" }, { "@id": "http://localhost:55001/plone/@controlpanels/markup", "group": "Content", + "searchable_text": [ + "Markup", + "Content", + "Default format", + "Alternative formats", + "Enabled markdown extensions" + ], "title": "Markup" }, { "@id": "http://localhost:55001/plone/@controlpanels/usergroup", "group": "Users", + "searchable_text": [ + "User and Group Settings", + "Users", + "Many groups?", + "Many users?" + ], "title": "User and Group Settings" }, { "@id": "http://localhost:55001/plone/@controlpanels/security", "group": "Security", + "searchable_text": [ + "Security", + "Security", + "Enable self-registration", + "Let users select their own passwords", + "Enable User Folders", + "Allow anyone to view 'about' information", + "Use email address as login name", + "Use UUID user ids", + "Login user after password reset" + ], "title": "Security" } ] From 45ccb8ab28c4508917fce24240821c82c7b36fcc Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 25 Feb 2026 17:09:56 -0800 Subject: [PATCH 8/9] Update news/1981.bugfix --- news/1981.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/1981.bugfix b/news/1981.bugfix index e2621d41d5..517f5da0de 100644 --- a/news/1981.bugfix +++ b/news/1981.bugfix @@ -1 +1 @@ -Added a single Control Panel API endpoint exposing searchable text for global settings search. @Manik-Khajuria-5 \ No newline at end of file +The `@controlpanel` service now includes `searchable_text` for each control panel. @Manik-Khajuria-5 \ No newline at end of file From 74c5b6bd7961ed5ee16972d4870199c739792768 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 25 Feb 2026 17:10:25 -0800 Subject: [PATCH 9/9] Rename 1981.bugfix to 1981.feature --- news/{1981.bugfix => 1981.feature} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename news/{1981.bugfix => 1981.feature} (62%) diff --git a/news/1981.bugfix b/news/1981.feature similarity index 62% rename from news/1981.bugfix rename to news/1981.feature index 517f5da0de..7a81cadfd5 100644 --- a/news/1981.bugfix +++ b/news/1981.feature @@ -1 +1 @@ -The `@controlpanel` service now includes `searchable_text` for each control panel. @Manik-Khajuria-5 \ No newline at end of file +The `@controlpanel` service now includes `searchable_text` for each control panel. @Manik-Khajuria-5