We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
searching for phrases within quotes doesn't work
the `munge_search_term` method doesn't work when searching for quoted phrases.

https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/browser/search.py

**Reproduce:**

- go to https://6-classic.demo.plone.org/en
- search for `"Welcome to Plone"`
- you get an empty search result

**Background**

the `munge_search_term` returns this for the phrase above:

`'Welcome AND to AND Plone*'`

**Solution**

keep phrases within quotes untouched.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `Products/CMFPlone/browser/search.py`
Content:
```
1 from DateTime import DateTime
2 from plone.app.contentlisting.interfaces import IContentListing
3 from plone.app.layout.navigation.interfaces import INavigationRoot
4 from plone.registry.interfaces import IRegistry
5 from Products.CMFCore.utils import getToolByName
6 from Products.CMFPlone.browser.navtree import getNavigationRoot
7 from plone.base.interfaces import ISearchSchema
8 from Products.CMFPlone.PloneBatch import Batch
9 from Products.ZCTextIndex.ParseTree import ParseError
10 from zope.cachedescriptors.property import Lazy as lazy_property
11 from zope.component import getMultiAdapter
12 from zope.component import getUtility
13 from zope.component import queryUtility
14 from zope.i18nmessageid import MessageFactory
15 from zope.publisher.browser import BrowserView
16 from ZTUtils import make_query
17 
18 import json
19 
20 _ = MessageFactory('plone')
21 
22 # We should accept both a simple space, unicode u'\u0020 but also a
23 # multi-space, so called 'waji-kankaku', unicode u'\u3000'
24 MULTISPACE = '\u3000'
25 BAD_CHARS = ('?', '-', '+', '*', MULTISPACE)
26 EVER = DateTime('1970-01-03')
27 
28 
29 def quote_chars(s):
30     # We need to quote parentheses when searching text indices
31     if '(' in s:
32         s = s.replace('(', '"("')
33     if ')' in s:
34         s = s.replace(')', '")"')
35     if MULTISPACE in s:
36         s = s.replace(MULTISPACE, ' ')
37     return s
38 
39 
40 def quote(term):
41     # The terms and, or and not must be wrapped in quotes to avoid
42     # being parsed as logical query atoms.
43     if term.lower() in ('and', 'or', 'not'):
44         term = '"%s"' % term
45     return term
46 
47 
48 class Search(BrowserView):
49 
50     valid_keys = ('sort_on', 'sort_order', 'sort_limit', 'fq', 'fl', 'facet')
51 
52     def munge_search_term(self, q):
53         for char in BAD_CHARS:
54             q = q.replace(char, ' ')
55         r = map(quote, q.split())
56         r = " AND ".join(r)
57         r = quote_chars(r) + '*'
58         return r
59 
60     def results(self, query=None, batch=True, b_size=10, b_start=0,
61                 use_content_listing=True):
62         """ Get properly wrapped search results from the catalog.
63         Everything in Plone that performs searches should go through this view.
64         'query' should be a dictionary of catalog parameters.
65         """
66         if query is None:
67             query = {}
68         if batch:
69             query['b_start'] = b_start = int(b_start)
70             query['b_size'] = b_size
71         query = self.filter_query(query)
72 
73         if query is None:
74             results = []
75         else:
76             catalog = getToolByName(self.context, 'portal_catalog')
77             try:
78                 results = catalog(**query)
79             except ParseError:
80                 return []
81 
82         if use_content_listing:
83             results = IContentListing(results)
84         if batch:
85             results = Batch(results, b_size, b_start)
86         return results
87 
88     def _filter_query(self, query):
89         request = self.request
90 
91         catalog = getToolByName(self.context, 'portal_catalog')
92         valid_indexes = tuple(catalog.indexes())
93         valid_keys = self.valid_keys + valid_indexes
94 
95         text = query.get('SearchableText', None)
96         if text is None:
97             text = request.form.get('SearchableText', '')
98         if not text:
99             # Without text, must provide a meaningful non-empty search
100             valid = set(valid_indexes).intersection(request.form.keys()) or \
101                 set(valid_indexes).intersection(query.keys())
102             if not valid:
103                 return
104 
105         for k, v in request.form.items():
106             if v and ((k in valid_keys) or k.startswith('facet.')):
107                 query[k] = v
108         if text:
109             query['SearchableText'] = self.munge_search_term(text)
110 
111         # don't filter on created at all if we want all results
112         created = query.get('created')
113         if created:
114             try:
115                 if created.get('query', EVER) <= EVER:
116                     del query['created']
117             except AttributeError:
118                 # created not a mapping
119                 del query['created']
120 
121         # respect `types_not_searched` setting
122         types = query.get('portal_type', [])
123         if 'query' in types:
124             types = types['query']
125         query['portal_type'] = self.filter_types(types)
126         # respect effective/expiration date
127         query['show_inactive'] = False
128         # respect navigation root
129         if 'path' not in query:
130             query['path'] = getNavigationRoot(self.context)
131 
132         if 'sort_order' in query and not query['sort_order']:
133             del query['sort_order']
134         return query
135 
136     @lazy_property
137     def default_sort_on(self):
138         registry = getUtility(IRegistry)
139         search_settings = registry.forInterface(ISearchSchema, prefix='plone')
140         return search_settings.sort_on
141 
142     def filter_query(self, query):
143         query = self._filter_query(query)
144         if query is None:
145             query = {}
146         # explicitly set a sort; if no `sort_on` is present, the catalog sorts
147         # by relevance
148         if 'sort_on' not in query:
149             self.default_sort_on
150             if self.default_sort_on != 'relevance':
151                 query['sort_on'] = self.default_sort_on
152         elif query['sort_on'] == 'relevance':
153             del query['sort_on']
154         if query.get('sort_on', '') == 'Date':
155             query['sort_order'] = 'reverse'
156         elif 'sort_order' in query:
157             del query['sort_order']
158         if not query:
159             return None
160         return query
161 
162     def filter_types(self, types):
163         plone_utils = getToolByName(self.context, 'plone_utils')
164         if not isinstance(types, list):
165             types = [types]
166         return plone_utils.getUserFriendlyTypes(types)
167 
168     def types_list(self):
169         # only show those types that have any content
170         catalog = getToolByName(self.context, 'portal_catalog')
171         used_types = catalog._catalog.getIndex('portal_type').uniqueValues()
172         return self.filter_types(list(used_types))
173 
174     def sort_options(self):
175         """ Sorting options for search results view. """
176         if 'sort_on' not in self.request.form:
177             self.request.form['sort_on'] = self.default_sort_on
178         return (
179             SortOption(self.request, _('relevance'), 'relevance'),
180             SortOption(
181                 self.request, _('date (newest first)'), 'Date', reverse=True
182             ),
183             SortOption(self.request, _('alphabetically'), 'sortable_title'),
184         )
185 
186     def show_advanced_search(self):
187         """Whether we need to show advanced search options a.k.a. filters?"""
188         show = self.request.get('advanced_search', None)
189         if not show or show == 'False':
190             return False
191         return True
192 
193     def advanced_search_trigger(self):
194         """URL builder for show/close advanced search filters."""
195         query = self.request.get('QUERY_STRING', None)
196         url = self.request.get('ACTUAL_URL', self.context.absolute_url())
197         if not query:
198             return url
199         if 'advanced_search' in query:
200             if 'advanced_search=True' in query:
201                 query = query.replace('advanced_search=True', '')
202             if 'advanced_search=False' in query:
203                 query = query.replace('advanced_search=False', '')
204         else:
205             query = query + '&advanced_search=True'
206         return url + '?' + query
207 
208     def breadcrumbs(self, item):
209         obj = item.getObject()
210         view = getMultiAdapter((obj, self.request), name='breadcrumbs_view')
211         # cut off the item itself
212         breadcrumbs = list(view.breadcrumbs())[:-1]
213         if len(breadcrumbs) == 0:
214             # don't show breadcrumbs if we only have a single element
215             return None
216         if len(breadcrumbs) > 3:
217             # if we have too long breadcrumbs, emit the middle elements
218             empty = {'absolute_url': '', 'Title': '…'}
219             breadcrumbs = [breadcrumbs[0], empty] + breadcrumbs[-2:]
220         return breadcrumbs
221 
222     def navroot_url(self):
223         if not hasattr(self, '_navroot_url'):
224             state = self.context.unrestrictedTraverse('@@plone_portal_state')
225             self._navroot_url = state.navigation_root_url()
226         return self._navroot_url
227 
228     @property
229     def show_images(self):
230         registry = queryUtility(IRegistry)
231         return registry.get('plone.search_show_images')
232 
233     @property
234     def search_image_scale(self):
235         registry = queryUtility(IRegistry)
236         return registry.get('plone.search_image_scale')
237 
238 
239 class AjaxSearch(Search):
240 
241     def __call__(self):
242         items = []
243         try:
244             per_page = int(self.request.form.get('perPage'))
245         except:
246             per_page = 10
247         try:
248             page = int(self.request.form.get('page'))
249         except:
250             page = 1
251 
252         results = self.results(batch=False, use_content_listing=False)
253         batch = Batch(results, per_page, start=(page - 1) * per_page)
254 
255         registry = queryUtility(IRegistry)
256         length = registry.get('plone.search_results_description_length')
257         show_images = registry.get('plone.search_show_images')
258         if show_images:
259             image_scale = registry.get('plone.search_image_scale')
260             # image_scaling = getMultiAdapter((self.context, self.request), name='image_scale')
261             self.image_scaling = getMultiAdapter((INavigationRoot(self.context), self.request), name='image_scale')
262         plone_view = getMultiAdapter(
263             (self.context, self.request), name='plone')
264         view_action_types = registry.get(
265             'plone.types_use_view_action_in_listings', [])
266         for item in batch:
267             url = item.getURL()
268             if item.portal_type in view_action_types:
269                 url = '%s/view' % url
270             img_tag = None
271             if show_images:
272                 img_tag = self.get_image_tag(item, image_scale)
273             items.append({
274                 'id': item.UID,
275                 'title': item.Title,
276                 'description': plone_view.cropText(item.Description, length),
277                 'url': url,
278                 'state': item.review_state if item.review_state else None,
279                 'img_tag': img_tag,
280             })
281         return json.dumps({
282             'total': len(results),
283             'items': items
284         })
285 
286     def get_image_tag(self, item, image_scale):
287         return self.image_scaling.tag(item, "image", scale=image_scale)
288 
289 class SortOption:
290 
291     def __init__(self, request, title, sortkey='', reverse=False):
292         self.request = request
293         self.title = title
294         self.sortkey = sortkey
295         self.reverse = reverse
296 
297     def selected(self):
298         sort_on = self.request.get('sort_on', '')
299         return sort_on == self.sortkey and sort_on != ''
300 
301     def url(self):
302         q = {}
303         q.update(self.request.form)
304         if 'sort_on' in q.keys():
305             del q['sort_on']
306         if 'sort_order' in q.keys():
307             del q['sort_order']
308         q['sort_on'] = self.sortkey
309         if self.reverse:
310             q['sort_order'] = 'reverse'
311 
312         base_url = self.request.URL
313         return base_url + '?' + make_query(q)
314
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/Products/CMFPlone/browser/search.py b/Products/CMFPlone/browser/search.py
--- a/Products/CMFPlone/browser/search.py
+++ b/Products/CMFPlone/browser/search.py
@@ -16,6 +16,7 @@
 from ZTUtils import make_query
 
 import json
+import re
 
 _ = MessageFactory('plone')
 
@@ -45,18 +46,33 @@
     return term
 
 
+def munge_search_term(query):
+    for char in BAD_CHARS:
+        query = query.replace(char, ' ')
+
+    # extract quoted phrases first
+    quoted_phrases = re.findall(r'"([^"]*)"', query)
+    r = []
+    for qp in quoted_phrases:
+        # remove from original query
+        query = query.replace(f'"{qp}"', "")
+        # replace with cleaned leading/trailing whitespaces
+        # and skip empty phrases
+        clean_qp = qp.strip()
+        if not clean_qp:
+            continue
+        r.append(f'"{clean_qp}"')
+
+    r += map(quote, query.strip().split())
+    r = " AND ".join(r)
+    r = quote_chars(r) + ('*' if r and not r.endswith('"') else '')
+    return r
+
+
 class Search(BrowserView):
 
     valid_keys = ('sort_on', 'sort_order', 'sort_limit', 'fq', 'fl', 'facet')
 
-    def munge_search_term(self, q):
-        for char in BAD_CHARS:
-            q = q.replace(char, ' ')
-        r = map(quote, q.split())
-        r = " AND ".join(r)
-        r = quote_chars(r) + '*'
-        return r
-
     def results(self, query=None, batch=True, b_size=10, b_start=0,
                 use_content_listing=True):
         """ Get properly wrapped search results from the catalog.
@@ -106,7 +122,7 @@
             if v and ((k in valid_keys) or k.startswith('facet.')):
                 query[k] = v
         if text:
-            query['SearchableText'] = self.munge_search_term(text)
+            query['SearchableText'] = munge_search_term(text)
 
         # don't filter on created at all if we want all results
         created = query.get('created')

{"golden_diff": "diff --git a/Products/CMFPlone/browser/search.py b/Products/CMFPlone/browser/search.py\n--- a/Products/CMFPlone/browser/search.py\n+++ b/Products/CMFPlone/browser/search.py\n@@ -16,6 +16,7 @@\n from ZTUtils import make_query\n \n import json\n+import re\n \n _ = MessageFactory('plone')\n \n@@ -45,18 +46,33 @@\n     return term\n \n \n+def munge_search_term(query):\n+    for char in BAD_CHARS:\n+        query = query.replace(char, ' ')\n+\n+    # extract quoted phrases first\n+    quoted_phrases = re.findall(r'\"([^\"]*)\"', query)\n+    r = []\n+    for qp in quoted_phrases:\n+        # remove from original query\n+        query = query.replace(f'\"{qp}\"', \"\")\n+        # replace with cleaned leading/trailing whitespaces\n+        # and skip empty phrases\n+        clean_qp = qp.strip()\n+        if not clean_qp:\n+            continue\n+        r.append(f'\"{clean_qp}\"')\n+\n+    r += map(quote, query.strip().split())\n+    r = \" AND \".join(r)\n+    r = quote_chars(r) + ('*' if r and not r.endswith('\"') else '')\n+    return r\n+\n+\n class Search(BrowserView):\n \n     valid_keys = ('sort_on', 'sort_order', 'sort_limit', 'fq', 'fl', 'facet')\n \n-    def munge_search_term(self, q):\n-        for char in BAD_CHARS:\n-            q = q.replace(char, ' ')\n-        r = map(quote, q.split())\n-        r = \" AND \".join(r)\n-        r = quote_chars(r) + '*'\n-        return r\n-\n     def results(self, query=None, batch=True, b_size=10, b_start=0,\n                 use_content_listing=True):\n         \"\"\" Get properly wrapped search results from the catalog.\n@@ -106,7 +122,7 @@\n             if v and ((k in valid_keys) or k.startswith('facet.')):\n                 query[k] = v\n         if text:\n-            query['SearchableText'] = self.munge_search_term(text)\n+            query['SearchableText'] = munge_search_term(text)\n \n         # don't filter on created at all if we want all results\n         created = query.get('created')\n", "issue": "searching for phrases within quotes doesn't work\nthe `munge_search_term` method doesn't work when searching for quoted phrases.\r\n\r\nhttps://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/browser/search.py\r\n\r\n**Reproduce:**\r\n\r\n- go to https://6-classic.demo.plone.org/en\r\n- search for `\"Welcome to Plone\"`\r\n- you get an empty search result\r\n\r\n**Background**\r\n\r\nthe `munge_search_term` returns this for the phrase above:\r\n\r\n`'Welcome AND to AND Plone*'`\r\n\r\n**Solution**\r\n\r\nkeep phrases within quotes untouched.\n", "before_files": [{"content": "from DateTime import DateTime\nfrom plone.app.contentlisting.interfaces import IContentListing\nfrom plone.app.layout.navigation.interfaces import INavigationRoot\nfrom plone.registry.interfaces import IRegistry\nfrom Products.CMFCore.utils import getToolByName\nfrom Products.CMFPlone.browser.navtree import getNavigationRoot\nfrom plone.base.interfaces import ISearchSchema\nfrom Products.CMFPlone.PloneBatch import Batch\nfrom Products.ZCTextIndex.ParseTree import ParseError\nfrom zope.cachedescriptors.property import Lazy as lazy_property\nfrom zope.component import getMultiAdapter\nfrom zope.component import getUtility\nfrom zope.component import queryUtility\nfrom zope.i18nmessageid import MessageFactory\nfrom zope.publisher.browser import BrowserView\nfrom ZTUtils import make_query\n\nimport json\n\n_ = MessageFactory('plone')\n\n# We should accept both a simple space, unicode u'\\u0020 but also a\n# multi-space, so called 'waji-kankaku', unicode u'\\u3000'\nMULTISPACE = '\\u3000'\nBAD_CHARS = ('?', '-', '+', '*', MULTISPACE)\nEVER = DateTime('1970-01-03')\n\n\ndef quote_chars(s):\n    # We need to quote parentheses when searching text indices\n    if '(' in s:\n        s = s.replace('(', '\"(\"')\n    if ')' in s:\n        s = s.replace(')', '\")\"')\n    if MULTISPACE in s:\n        s = s.replace(MULTISPACE, ' ')\n    return s\n\n\ndef quote(term):\n    # The terms and, or and not must be wrapped in quotes to avoid\n    # being parsed as logical query atoms.\n    if term.lower() in ('and', 'or', 'not'):\n        term = '\"%s\"' % term\n    return term\n\n\nclass Search(BrowserView):\n\n    valid_keys = ('sort_on', 'sort_order', 'sort_limit', 'fq', 'fl', 'facet')\n\n    def munge_search_term(self, q):\n        for char in BAD_CHARS:\n            q = q.replace(char, ' ')\n        r = map(quote, q.split())\n        r = \" AND \".join(r)\n        r = quote_chars(r) + '*'\n        return r\n\n    def results(self, query=None, batch=True, b_size=10, b_start=0,\n                use_content_listing=True):\n        \"\"\" Get properly wrapped search results from the catalog.\n        Everything in Plone that performs searches should go through this view.\n        'query' should be a dictionary of catalog parameters.\n        \"\"\"\n        if query is None:\n            query = {}\n        if batch:\n            query['b_start'] = b_start = int(b_start)\n            query['b_size'] = b_size\n        query = self.filter_query(query)\n\n        if query is None:\n            results = []\n        else:\n            catalog = getToolByName(self.context, 'portal_catalog')\n            try:\n                results = catalog(**query)\n            except ParseError:\n                return []\n\n        if use_content_listing:\n            results = IContentListing(results)\n        if batch:\n            results = Batch(results, b_size, b_start)\n        return results\n\n    def _filter_query(self, query):\n        request = self.request\n\n        catalog = getToolByName(self.context, 'portal_catalog')\n        valid_indexes = tuple(catalog.indexes())\n        valid_keys = self.valid_keys + valid_indexes\n\n        text = query.get('SearchableText', None)\n        if text is None:\n            text = request.form.get('SearchableText', '')\n        if not text:\n            # Without text, must provide a meaningful non-empty search\n            valid = set(valid_indexes).intersection(request.form.keys()) or \\\n                set(valid_indexes).intersection(query.keys())\n            if not valid:\n                return\n\n        for k, v in request.form.items():\n            if v and ((k in valid_keys) or k.startswith('facet.')):\n                query[k] = v\n        if text:\n            query['SearchableText'] = self.munge_search_term(text)\n\n        # don't filter on created at all if we want all results\n        created = query.get('created')\n        if created:\n            try:\n                if created.get('query', EVER) <= EVER:\n                    del query['created']\n            except AttributeError:\n                # created not a mapping\n                del query['created']\n\n        # respect `types_not_searched` setting\n        types = query.get('portal_type', [])\n        if 'query' in types:\n            types = types['query']\n        query['portal_type'] = self.filter_types(types)\n        # respect effective/expiration date\n        query['show_inactive'] = False\n        # respect navigation root\n        if 'path' not in query:\n            query['path'] = getNavigationRoot(self.context)\n\n        if 'sort_order' in query and not query['sort_order']:\n            del query['sort_order']\n        return query\n\n    @lazy_property\n    def default_sort_on(self):\n        registry = getUtility(IRegistry)\n        search_settings = registry.forInterface(ISearchSchema, prefix='plone')\n        return search_settings.sort_on\n\n    def filter_query(self, query):\n        query = self._filter_query(query)\n        if query is None:\n            query = {}\n        # explicitly set a sort; if no `sort_on` is present, the catalog sorts\n        # by relevance\n        if 'sort_on' not in query:\n            self.default_sort_on\n            if self.default_sort_on != 'relevance':\n                query['sort_on'] = self.default_sort_on\n        elif query['sort_on'] == 'relevance':\n            del query['sort_on']\n        if query.get('sort_on', '') == 'Date':\n            query['sort_order'] = 'reverse'\n        elif 'sort_order' in query:\n            del query['sort_order']\n        if not query:\n            return None\n        return query\n\n    def filter_types(self, types):\n        plone_utils = getToolByName(self.context, 'plone_utils')\n        if not isinstance(types, list):\n            types = [types]\n        return plone_utils.getUserFriendlyTypes(types)\n\n    def types_list(self):\n        # only show those types that have any content\n        catalog = getToolByName(self.context, 'portal_catalog')\n        used_types = catalog._catalog.getIndex('portal_type').uniqueValues()\n        return self.filter_types(list(used_types))\n\n    def sort_options(self):\n        \"\"\" Sorting options for search results view. \"\"\"\n        if 'sort_on' not in self.request.form:\n            self.request.form['sort_on'] = self.default_sort_on\n        return (\n            SortOption(self.request, _('relevance'), 'relevance'),\n            SortOption(\n                self.request, _('date (newest first)'), 'Date', reverse=True\n            ),\n            SortOption(self.request, _('alphabetically'), 'sortable_title'),\n        )\n\n    def show_advanced_search(self):\n        \"\"\"Whether we need to show advanced search options a.k.a. filters?\"\"\"\n        show = self.request.get('advanced_search', None)\n        if not show or show == 'False':\n            return False\n        return True\n\n    def advanced_search_trigger(self):\n        \"\"\"URL builder for show/close advanced search filters.\"\"\"\n        query = self.request.get('QUERY_STRING', None)\n        url = self.request.get('ACTUAL_URL', self.context.absolute_url())\n        if not query:\n            return url\n        if 'advanced_search' in query:\n            if 'advanced_search=True' in query:\n                query = query.replace('advanced_search=True', '')\n            if 'advanced_search=False' in query:\n                query = query.replace('advanced_search=False', '')\n        else:\n            query = query + '&advanced_search=True'\n        return url + '?' + query\n\n    def breadcrumbs(self, item):\n        obj = item.getObject()\n        view = getMultiAdapter((obj, self.request), name='breadcrumbs_view')\n        # cut off the item itself\n        breadcrumbs = list(view.breadcrumbs())[:-1]\n        if len(breadcrumbs) == 0:\n            # don't show breadcrumbs if we only have a single element\n            return None\n        if len(breadcrumbs) > 3:\n            # if we have too long breadcrumbs, emit the middle elements\n            empty = {'absolute_url': '', 'Title': '\u2026'}\n            breadcrumbs = [breadcrumbs[0], empty] + breadcrumbs[-2:]\n        return breadcrumbs\n\n    def navroot_url(self):\n        if not hasattr(self, '_navroot_url'):\n            state = self.context.unrestrictedTraverse('@@plone_portal_state')\n            self._navroot_url = state.navigation_root_url()\n        return self._navroot_url\n\n    @property\n    def show_images(self):\n        registry = queryUtility(IRegistry)\n        return registry.get('plone.search_show_images')\n\n    @property\n    def search_image_scale(self):\n        registry = queryUtility(IRegistry)\n        return registry.get('plone.search_image_scale')\n\n\nclass AjaxSearch(Search):\n\n    def __call__(self):\n        items = []\n        try:\n            per_page = int(self.request.form.get('perPage'))\n        except:\n            per_page = 10\n        try:\n            page = int(self.request.form.get('page'))\n        except:\n            page = 1\n\n        results = self.results(batch=False, use_content_listing=False)\n        batch = Batch(results, per_page, start=(page - 1) * per_page)\n\n        registry = queryUtility(IRegistry)\n        length = registry.get('plone.search_results_description_length')\n        show_images = registry.get('plone.search_show_images')\n        if show_images:\n            image_scale = registry.get('plone.search_image_scale')\n            # image_scaling = getMultiAdapter((self.context, self.request), name='image_scale')\n            self.image_scaling = getMultiAdapter((INavigationRoot(self.context), self.request), name='image_scale')\n        plone_view = getMultiAdapter(\n            (self.context, self.request), name='plone')\n        view_action_types = registry.get(\n            'plone.types_use_view_action_in_listings', [])\n        for item in batch:\n            url = item.getURL()\n            if item.portal_type in view_action_types:\n                url = '%s/view' % url\n            img_tag = None\n            if show_images:\n                img_tag = self.get_image_tag(item, image_scale)\n            items.append({\n                'id': item.UID,\n                'title': item.Title,\n                'description': plone_view.cropText(item.Description, length),\n                'url': url,\n                'state': item.review_state if item.review_state else None,\n                'img_tag': img_tag,\n            })\n        return json.dumps({\n            'total': len(results),\n            'items': items\n        })\n\n    def get_image_tag(self, item, image_scale):\n        return self.image_scaling.tag(item, \"image\", scale=image_scale)\n\nclass SortOption:\n\n    def __init__(self, request, title, sortkey='', reverse=False):\n        self.request = request\n        self.title = title\n        self.sortkey = sortkey\n        self.reverse = reverse\n\n    def selected(self):\n        sort_on = self.request.get('sort_on', '')\n        return sort_on == self.sortkey and sort_on != ''\n\n    def url(self):\n        q = {}\n        q.update(self.request.form)\n        if 'sort_on' in q.keys():\n            del q['sort_on']\n        if 'sort_order' in q.keys():\n            del q['sort_order']\n        q['sort_on'] = self.sortkey\n        if self.reverse:\n            q['sort_order'] = 'reverse'\n\n        base_url = self.request.URL\n        return base_url + '?' + make_query(q)\n", "path": "Products/CMFPlone/browser/search.py"}], "after_files": [{"content": "from DateTime import DateTime\nfrom plone.app.contentlisting.interfaces import IContentListing\nfrom plone.app.layout.navigation.interfaces import INavigationRoot\nfrom plone.registry.interfaces import IRegistry\nfrom Products.CMFCore.utils import getToolByName\nfrom Products.CMFPlone.browser.navtree import getNavigationRoot\nfrom plone.base.interfaces import ISearchSchema\nfrom Products.CMFPlone.PloneBatch import Batch\nfrom Products.ZCTextIndex.ParseTree import ParseError\nfrom zope.cachedescriptors.property import Lazy as lazy_property\nfrom zope.component import getMultiAdapter\nfrom zope.component import getUtility\nfrom zope.component import queryUtility\nfrom zope.i18nmessageid import MessageFactory\nfrom zope.publisher.browser import BrowserView\nfrom ZTUtils import make_query\n\nimport json\nimport re\n\n_ = MessageFactory('plone')\n\n# We should accept both a simple space, unicode u'\\u0020 but also a\n# multi-space, so called 'waji-kankaku', unicode u'\\u3000'\nMULTISPACE = '\\u3000'\nBAD_CHARS = ('?', '-', '+', '*', MULTISPACE)\nEVER = DateTime('1970-01-03')\n\n\ndef quote_chars(s):\n    # We need to quote parentheses when searching text indices\n    if '(' in s:\n        s = s.replace('(', '\"(\"')\n    if ')' in s:\n        s = s.replace(')', '\")\"')\n    if MULTISPACE in s:\n        s = s.replace(MULTISPACE, ' ')\n    return s\n\n\ndef quote(term):\n    # The terms and, or and not must be wrapped in quotes to avoid\n    # being parsed as logical query atoms.\n    if term.lower() in ('and', 'or', 'not'):\n        term = '\"%s\"' % term\n    return term\n\n\ndef munge_search_term(query):\n    for char in BAD_CHARS:\n        query = query.replace(char, ' ')\n\n    # extract quoted phrases first\n    quoted_phrases = re.findall(r'\"([^\"]*)\"', query)\n    r = []\n    for qp in quoted_phrases:\n        # remove from original query\n        query = query.replace(f'\"{qp}\"', \"\")\n        # replace with cleaned leading/trailing whitespaces\n        # and skip empty phrases\n        clean_qp = qp.strip()\n        if not clean_qp:\n            continue\n        r.append(f'\"{clean_qp}\"')\n\n    r += map(quote, query.strip().split())\n    r = \" AND \".join(r)\n    r = quote_chars(r) + ('*' if r and not r.endswith('\"') else '')\n    return r\n\n\nclass Search(BrowserView):\n\n    valid_keys = ('sort_on', 'sort_order', 'sort_limit', 'fq', 'fl', 'facet')\n\n    def results(self, query=None, batch=True, b_size=10, b_start=0,\n                use_content_listing=True):\n        \"\"\" Get properly wrapped search results from the catalog.\n        Everything in Plone that performs searches should go through this view.\n        'query' should be a dictionary of catalog parameters.\n        \"\"\"\n        if query is None:\n            query = {}\n        if batch:\n            query['b_start'] = b_start = int(b_start)\n            query['b_size'] = b_size\n        query = self.filter_query(query)\n\n        if query is None:\n            results = []\n        else:\n            catalog = getToolByName(self.context, 'portal_catalog')\n            try:\n                results = catalog(**query)\n            except ParseError:\n                return []\n\n        if use_content_listing:\n            results = IContentListing(results)\n        if batch:\n            results = Batch(results, b_size, b_start)\n        return results\n\n    def _filter_query(self, query):\n        request = self.request\n\n        catalog = getToolByName(self.context, 'portal_catalog')\n        valid_indexes = tuple(catalog.indexes())\n        valid_keys = self.valid_keys + valid_indexes\n\n        text = query.get('SearchableText', None)\n        if text is None:\n            text = request.form.get('SearchableText', '')\n        if not text:\n            # Without text, must provide a meaningful non-empty search\n            valid = set(valid_indexes).intersection(request.form.keys()) or \\\n                set(valid_indexes).intersection(query.keys())\n            if not valid:\n                return\n\n        for k, v in request.form.items():\n            if v and ((k in valid_keys) or k.startswith('facet.')):\n                query[k] = v\n        if text:\n            query['SearchableText'] = munge_search_term(text)\n\n        # don't filter on created at all if we want all results\n        created = query.get('created')\n        if created:\n            try:\n                if created.get('query', EVER) <= EVER:\n                    del query['created']\n            except AttributeError:\n                # created not a mapping\n                del query['created']\n\n        # respect `types_not_searched` setting\n        types = query.get('portal_type', [])\n        if 'query' in types:\n            types = types['query']\n        query['portal_type'] = self.filter_types(types)\n        # respect effective/expiration date\n        query['show_inactive'] = False\n        # respect navigation root\n        if 'path' not in query:\n            query['path'] = getNavigationRoot(self.context)\n\n        if 'sort_order' in query and not query['sort_order']:\n            del query['sort_order']\n        return query\n\n    @lazy_property\n    def default_sort_on(self):\n        registry = getUtility(IRegistry)\n        search_settings = registry.forInterface(ISearchSchema, prefix='plone')\n        return search_settings.sort_on\n\n    def filter_query(self, query):\n        query = self._filter_query(query)\n        if query is None:\n            query = {}\n        # explicitly set a sort; if no `sort_on` is present, the catalog sorts\n        # by relevance\n        if 'sort_on' not in query:\n            self.default_sort_on\n            if self.default_sort_on != 'relevance':\n                query['sort_on'] = self.default_sort_on\n        elif query['sort_on'] == 'relevance':\n            del query['sort_on']\n        if query.get('sort_on', '') == 'Date':\n            query['sort_order'] = 'reverse'\n        elif 'sort_order' in query:\n            del query['sort_order']\n        if not query:\n            return None\n        return query\n\n    def filter_types(self, types):\n        plone_utils = getToolByName(self.context, 'plone_utils')\n        if not isinstance(types, list):\n            types = [types]\n        return plone_utils.getUserFriendlyTypes(types)\n\n    def types_list(self):\n        # only show those types that have any content\n        catalog = getToolByName(self.context, 'portal_catalog')\n        used_types = catalog._catalog.getIndex('portal_type').uniqueValues()\n        return self.filter_types(list(used_types))\n\n    def sort_options(self):\n        \"\"\" Sorting options for search results view. \"\"\"\n        if 'sort_on' not in self.request.form:\n            self.request.form['sort_on'] = self.default_sort_on\n        return (\n            SortOption(self.request, _('relevance'), 'relevance'),\n            SortOption(\n                self.request, _('date (newest first)'), 'Date', reverse=True\n            ),\n            SortOption(self.request, _('alphabetically'), 'sortable_title'),\n        )\n\n    def show_advanced_search(self):\n        \"\"\"Whether we need to show advanced search options a.k.a. filters?\"\"\"\n        show = self.request.get('advanced_search', None)\n        if not show or show == 'False':\n            return False\n        return True\n\n    def advanced_search_trigger(self):\n        \"\"\"URL builder for show/close advanced search filters.\"\"\"\n        query = self.request.get('QUERY_STRING', None)\n        url = self.request.get('ACTUAL_URL', self.context.absolute_url())\n        if not query:\n            return url\n        if 'advanced_search' in query:\n            if 'advanced_search=True' in query:\n                query = query.replace('advanced_search=True', '')\n            if 'advanced_search=False' in query:\n                query = query.replace('advanced_search=False', '')\n        else:\n            query = query + '&advanced_search=True'\n        return url + '?' + query\n\n    def breadcrumbs(self, item):\n        obj = item.getObject()\n        view = getMultiAdapter((obj, self.request), name='breadcrumbs_view')\n        # cut off the item itself\n        breadcrumbs = list(view.breadcrumbs())[:-1]\n        if len(breadcrumbs) == 0:\n            # don't show breadcrumbs if we only have a single element\n            return None\n        if len(breadcrumbs) > 3:\n            # if we have too long breadcrumbs, emit the middle elements\n            empty = {'absolute_url': '', 'Title': '\u2026'}\n            breadcrumbs = [breadcrumbs[0], empty] + breadcrumbs[-2:]\n        return breadcrumbs\n\n    def navroot_url(self):\n        if not hasattr(self, '_navroot_url'):\n            state = self.context.unrestrictedTraverse('@@plone_portal_state')\n            self._navroot_url = state.navigation_root_url()\n        return self._navroot_url\n\n    @property\n    def show_images(self):\n        registry = queryUtility(IRegistry)\n        return registry.get('plone.search_show_images')\n\n    @property\n    def search_image_scale(self):\n        registry = queryUtility(IRegistry)\n        return registry.get('plone.search_image_scale')\n\n\nclass AjaxSearch(Search):\n\n    def __call__(self):\n        items = []\n        try:\n            per_page = int(self.request.form.get('perPage'))\n        except:\n            per_page = 10\n        try:\n            page = int(self.request.form.get('page'))\n        except:\n            page = 1\n\n        results = self.results(batch=False, use_content_listing=False)\n        batch = Batch(results, per_page, start=(page - 1) * per_page)\n\n        registry = queryUtility(IRegistry)\n        length = registry.get('plone.search_results_description_length')\n        show_images = registry.get('plone.search_show_images')\n        if show_images:\n            image_scale = registry.get('plone.search_image_scale')\n            # image_scaling = getMultiAdapter((self.context, self.request), name='image_scale')\n            self.image_scaling = getMultiAdapter((INavigationRoot(self.context), self.request), name='image_scale')\n        plone_view = getMultiAdapter(\n            (self.context, self.request), name='plone')\n        view_action_types = registry.get(\n            'plone.types_use_view_action_in_listings', [])\n        for item in batch:\n            url = item.getURL()\n            if item.portal_type in view_action_types:\n                url = '%s/view' % url\n            img_tag = None\n            if show_images:\n                img_tag = self.get_image_tag(item, image_scale)\n            items.append({\n                'id': item.UID,\n                'title': item.Title,\n                'description': plone_view.cropText(item.Description, length),\n                'url': url,\n                'state': item.review_state if item.review_state else None,\n                'img_tag': img_tag,\n            })\n        return json.dumps({\n            'total': len(results),\n            'items': items\n        })\n\n    def get_image_tag(self, item, image_scale):\n        return self.image_scaling.tag(item, \"image\", scale=image_scale)\n\nclass SortOption:\n\n    def __init__(self, request, title, sortkey='', reverse=False):\n        self.request = request\n        self.title = title\n        self.sortkey = sortkey\n        self.reverse = reverse\n\n    def selected(self):\n        sort_on = self.request.get('sort_on', '')\n        return sort_on == self.sortkey and sort_on != ''\n\n    def url(self):\n        q = {}\n        q.update(self.request.form)\n        if 'sort_on' in q.keys():\n            del q['sort_on']\n        if 'sort_order' in q.keys():\n            del q['sort_order']\n        q['sort_on'] = self.sortkey\n        if self.reverse:\n            q['sort_order'] = 'reverse'\n\n        base_url = self.request.URL\n        return base_url + '?' + make_query(q)\n", "path": "Products/CMFPlone/browser/search.py"}]}