1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import pkg_resources
18 import re
19
20 from genshi.builder import tag
21
22 from trac.config import IntOption, ListOption
23 from trac.core import *
24 from trac.perm import IPermissionRequestor
25 from trac.search.api import ISearchSource
26 from trac.util import as_int
27 from trac.util.datefmt import format_datetime, user_time
28 from trac.util.html import find_element
29 from trac.util.presentation import Paginator
30 from trac.util.text import quote_query_string
31 from trac.util.translation import _
32 from trac.web.api import IRequestHandler
33 from trac.web.chrome import (INavigationContributor, ITemplateProvider,
34 add_link, add_stylesheet, add_warning,
35 web_context)
36 from trac.wiki.api import IWikiSyntaxProvider
37 from trac.wiki.formatter import extract_link
38
39
41 """Controller for the search sub-system"""
42
43 implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
44 ITemplateProvider, IWikiSyntaxProvider)
45
46 search_sources = ExtensionPoint(ISearchSource)
47
48 RESULTS_PER_PAGE = 10
49
50 min_query_length = IntOption('search', 'min_query_length', 3,
51 """Minimum length of query string allowed when performing a search.""")
52
53 default_disabled_filters = ListOption('search', 'default_disabled_filters',
54 doc="""Specifies which search filters should be disabled by
55 default on the search page. This will also restrict the
56 filters for the quick search function. The filter names
57 defined by default components are: `wiki`, `ticket`,
58 `milestone` and `changeset`. For plugins, look for
59 their implementation of the ISearchSource interface, in
60 the `get_search_filters()` method, the first member of
61 returned tuple. Once disabled, search filters can still
62 be manually enabled by the user on the search page.
63 (since 0.12)""")
64
65
66
69
71 if 'SEARCH_VIEW' in req.perm:
72 yield ('mainnav', 'search',
73 tag.a(_('Search'), href=req.href.search(), accesskey=4))
74
75
76
78 return ['SEARCH_VIEW']
79
80
81
83 return re.match(r'/search(?:/opensearch)?$', req.path_info) is not None
84
86 req.perm.assert_permission('SEARCH_VIEW')
87
88 if req.path_info == '/search/opensearch':
89 return ('opensearch.xml', {},
90 'application/opensearchdescription+xml')
91
92 query = req.args.getfirst('q')
93 available_filters = []
94 for source in self.search_sources:
95 available_filters.extend(source.get_search_filters(req) or [])
96 available_filters.sort(key=lambda f: f[1].lower())
97
98 filters = self._get_selected_filters(req, available_filters)
99 data = self._prepare_data(req, query, available_filters, filters)
100 if query:
101 data['quickjump'] = self._check_quickjump(req, query)
102 if query.startswith('!'):
103 query = query[1:]
104
105 terms = self._parse_query(req, query)
106 if terms:
107 results = self._do_search(req, terms, filters)
108 if results:
109 data.update(self._prepare_results(req, filters, results))
110
111 add_stylesheet(req, 'common/css/search.css')
112 return 'search.html', data, None
113
114
115
118
120 return [pkg_resources.resource_filename('trac.search', 'templates')]
121
122
123
126
128 yield ('search', self._format_link)
129
140
141
142
144 """Return selected filters or the default filters if none was selected.
145 """
146 filters = [f[0] for f in available_filters if f[0] in req.args]
147 if not filters:
148 filters = [f[0] for f in available_filters
149 if f[0] not in self.default_disabled_filters and
150 (len(f) < 3 or len(f) > 2 and f[2])]
151 return filters
152
153 - def _prepare_data(self, req, query, available_filters, filters):
154 return {'filters': [{'name': f[0], 'label': f[1],
155 'active': f[0] in filters}
156 for f in available_filters],
157 'query': query, 'quickjump': None, 'results': []}
158
160 """Look for search shortcuts"""
161 noquickjump = as_int(req.args.get('noquickjump'), 0)
162
163 quickjump_href = None
164 if kwd[0] == '/':
165 quickjump_href = req.href.browser(kwd)
166 name = kwd
167 description = _('Browse repository path %(path)s', path=kwd)
168 else:
169 context = web_context(req, 'search')
170 link = find_element(extract_link(self.env, context, kwd), 'href')
171 if link is not None:
172 quickjump_href = link.attrib.get('href')
173 name = link.children
174 description = link.attrib.get('title', '')
175 if quickjump_href:
176
177 if not quickjump_href.startswith(req.base_path or '/'):
178 noquickjump = True
179 if noquickjump:
180 return {'href': quickjump_href, 'name': tag.em(name),
181 'description': description}
182 else:
183 req.redirect(quickjump_href)
184
186 """Break apart a search query into its various search terms.
187
188 Terms are grouped implicitly by word boundary, or explicitly by (single
189 or double) quotes.
190 """
191 terms = []
192 for term in re.split('(".*?")|(\'.*?\')|(\s+)', query):
193 if term is not None and term.strip():
194 if term[0] == term[-1] and term[0] in "'\"":
195 term = term[1:-1]
196 terms.append(term)
197 return terms
198
200 """Parse query and refuse those which would result in a huge result set
201 """
202 terms = self._get_search_terms(query)
203 if terms and (len(terms) > 1 or
204 len(terms[0]) >= self.min_query_length):
205 return terms
206
207 add_warning(req, _('Search query too short. '
208 'Query must be at least %(num)s characters long.',
209 num=self.min_query_length))
210
217
219 page = req.args.get('page', 1)
220 page = as_int(page, default=1, min=1)
221 try:
222 results = Paginator(results, page - 1, self.RESULTS_PER_PAGE)
223 except TracError:
224 add_warning(req, _("Page %(page)s is out of range.", page=page))
225 page = 1
226 results = Paginator(results, page - 1, self.RESULTS_PER_PAGE)
227
228 for idx, result in enumerate(results):
229 results[idx] = {'href': result[0], 'title': result[1],
230 'date': user_time(req, format_datetime, result[2]),
231 'author': result[3], 'excerpt': result[4]}
232
233 pagedata = []
234 shown_pages = results.get_shown_pages(21)
235 for shown_page in shown_pages:
236 page_href = req.href.search([(f, 'on') for f in filters],
237 q=req.args.get('q'),
238 page=shown_page, noquickjump=1)
239 pagedata.append([page_href, None, str(shown_page),
240 _("Page %(num)d", num=shown_page)])
241
242 fields = ['href', 'class', 'string', 'title']
243 results.shown_pages = [dict(zip(fields, p)) for p in pagedata]
244
245 results.current_page = {'href': None, 'class': 'current',
246 'string': str(results.page + 1),
247 'title': None}
248
249 if results.has_next_page:
250 next_href = req.href.search(zip(filters, ['on'] * len(filters)),
251 q=req.args.get('q'), page=page + 1,
252 noquickjump=1)
253 add_link(req, 'next', next_href, _('Next Page'))
254
255 if results.has_previous_page:
256 prev_href = req.href.search(zip(filters, ['on'] * len(filters)),
257 q=req.args.get('q'), page=page - 1,
258 noquickjump=1)
259 add_link(req, 'prev', prev_href, _('Previous Page'))
260
261 page_href = req.href.search(
262 zip(filters, ['on'] * len(filters)), q=req.args.get('q'),
263 noquickjump=1)
264 return {'results': results, 'page_href': page_href}
265