##// END OF EJS Templates
search: goto commit search will now use a safe search option and never...
marcink -
r1411:16beb154 default
parent child Browse files
Show More
@@ -1,288 +1,290 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Home controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 import time
27 27 import re
28 28
29 29 from pylons import tmpl_context as c, request, url, config
30 30 from pylons.i18n.translation import _
31 31 from sqlalchemy.sql import func
32 32
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.lib.index import searcher_from_config
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.utils import jsonify
40 40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 41 from rhodecode.model.db import Repository, RepoGroup
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.scm import RepoList, RepoGroupList
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HomeController(BaseController):
51 51 def __before__(self):
52 52 super(HomeController, self).__before__()
53 53
54 54 def ping(self):
55 55 """
56 56 Ping, doesn't require login, good for checking out the platform
57 57 """
58 58 instance_id = getattr(c, 'rhodecode_instanceid', '')
59 59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
60 60
61 61 @LoginRequired()
62 62 @HasPermissionAllDecorator('hg.admin')
63 63 def error_test(self):
64 64 """
65 65 Test exception handling and emails on errors
66 66 """
67 67 class TestException(Exception):
68 68 pass
69 69
70 70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
71 71 % (c.rhodecode_name, time.time()))
72 72 raise TestException(msg)
73 73
74 74 def _get_groups_and_repos(self, repo_group_id=None):
75 75 # repo groups groups
76 76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
77 77 _perms = ['group.read', 'group.write', 'group.admin']
78 78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
79 79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
80 80 repo_group_list=repo_group_list_acl, admin=False)
81 81
82 82 # repositories
83 83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
84 84 _perms = ['repository.read', 'repository.write', 'repository.admin']
85 85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
86 86 repo_data = RepoModel().get_repos_as_dict(
87 87 repo_list=repo_list_acl, admin=False)
88 88
89 89 return repo_data, repo_group_data
90 90
91 91 @LoginRequired()
92 92 def index(self):
93 93 c.repo_group = None
94 94
95 95 repo_data, repo_group_data = self._get_groups_and_repos()
96 96 # json used to render the grids
97 97 c.repos_data = json.dumps(repo_data)
98 98 c.repo_groups_data = json.dumps(repo_group_data)
99 99
100 100 return render('/index.mako')
101 101
102 102 @LoginRequired()
103 103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
104 104 'group.admin')
105 105 def index_repo_group(self, group_name):
106 106 """GET /repo_group_name: Show a specific item"""
107 107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
108 108 repo_data, repo_group_data = self._get_groups_and_repos(
109 109 c.repo_group.group_id)
110 110
111 111 # json used to render the grids
112 112 c.repos_data = json.dumps(repo_data)
113 113 c.repo_groups_data = json.dumps(repo_group_data)
114 114
115 115 return render('index_repo_group.mako')
116 116
117 117 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
118 118 query = Repository.query()\
119 119 .order_by(func.length(Repository.repo_name))\
120 120 .order_by(Repository.repo_name)
121 121
122 122 if repo_type:
123 123 query = query.filter(Repository.repo_type == repo_type)
124 124
125 125 if name_contains:
126 126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 127 query = query.filter(
128 128 Repository.repo_name.ilike(ilike_expression))
129 129 query = query.limit(limit)
130 130
131 131 all_repos = query.all()
132 132 repo_iter = self.scm_model.get_repos(all_repos)
133 133 return [
134 134 {
135 135 'id': obj['name'],
136 136 'text': obj['name'],
137 137 'type': 'repo',
138 138 'obj': obj['dbrepo'],
139 139 'url': url('summary_home', repo_name=obj['name'])
140 140 }
141 141 for obj in repo_iter]
142 142
143 143 def _get_repo_group_list(self, name_contains=None, limit=20):
144 144 query = RepoGroup.query()\
145 145 .order_by(func.length(RepoGroup.group_name))\
146 146 .order_by(RepoGroup.group_name)
147 147
148 148 if name_contains:
149 149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
150 150 query = query.filter(
151 151 RepoGroup.group_name.ilike(ilike_expression))
152 152 query = query.limit(limit)
153 153
154 154 all_groups = query.all()
155 155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
156 156 return [
157 157 {
158 158 'id': obj.group_name,
159 159 'text': obj.group_name,
160 160 'type': 'group',
161 161 'obj': {},
162 162 'url': url('repo_group_home', group_name=obj.group_name)
163 163 }
164 164 for obj in repo_groups_iter]
165 165
166 166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
167 167 if not hash_starts_with or len(hash_starts_with) < 3:
168 168 return []
169 169
170 170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
171 171
172 172 if len(commit_hashes) != 1:
173 173 return []
174 174
175 175 commit_hash_prefix = commit_hashes[0]
176 176
177 177 auth_user = AuthUser(
178 178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
179 179 searcher = searcher_from_config(config)
180 180 result = searcher.search(
181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
182 raise_on_exc=False)
182 183
183 184 return [
184 185 {
185 186 'id': entry['commit_id'],
186 187 'text': entry['commit_id'],
187 188 'type': 'commit',
188 189 'obj': {'repo': entry['repository']},
189 190 'url': url('changeset_home',
190 repo_name=entry['repository'], revision=entry['commit_id'])
191 repo_name=entry['repository'],
192 revision=entry['commit_id'])
191 193 }
192 194 for entry in result['results']]
193 195
194 196 @LoginRequired()
195 197 @XHRRequired()
196 198 @jsonify
197 199 def goto_switcher_data(self):
198 200 query = request.GET.get('query')
199 201 log.debug('generating goto switcher list, query %s', query)
200 202
201 203 res = []
202 204 repo_groups = self._get_repo_group_list(query)
203 205 if repo_groups:
204 206 res.append({
205 207 'text': _('Groups'),
206 208 'children': repo_groups
207 209 })
208 210
209 211 repos = self._get_repo_list(query)
210 212 if repos:
211 213 res.append({
212 214 'text': _('Repositories'),
213 215 'children': repos
214 216 })
215 217
216 218 commits = self._get_hash_commit_list(query)
217 219 if commits:
218 220 unique_repos = {}
219 221 for commit in commits:
220 222 unique_repos.setdefault(commit['obj']['repo'], []
221 223 ).append(commit)
222 224
223 225 for repo in unique_repos:
224 226 res.append({
225 227 'text': _('Commits in %(repo)s') % {'repo': repo},
226 228 'children': unique_repos[repo]
227 229 })
228 230
229 231 data = {
230 232 'more': False,
231 233 'results': res
232 234 }
233 235 return data
234 236
235 237 @LoginRequired()
236 238 @XHRRequired()
237 239 @jsonify
238 240 def repo_list_data(self):
239 241 query = request.GET.get('query')
240 242 repo_type = request.GET.get('repo_type')
241 243 log.debug('generating repo list, query:%s', query)
242 244
243 245 res = []
244 246 repos = self._get_repo_list(query, repo_type=repo_type)
245 247 if repos:
246 248 res.append({
247 249 'text': _('Repositories'),
248 250 'children': repos
249 251 })
250 252
251 253 data = {
252 254 'more': False,
253 255 'results': res
254 256 }
255 257 return data
256 258
257 259 @LoginRequired()
258 260 @XHRRequired()
259 261 @jsonify
260 262 def user_autocomplete_data(self):
261 263 query = request.GET.get('query')
262 264 active = str2bool(request.GET.get('active') or True)
263 265
264 266 repo_model = RepoModel()
265 267 _users = repo_model.get_users(
266 268 name_contains=query, only_active=active)
267 269
268 270 if request.GET.get('user_groups'):
269 271 # extend with user groups
270 272 _user_groups = repo_model.get_user_groups(
271 273 name_contains=query, only_active=active)
272 274 _users = _users + _user_groups
273 275
274 276 return {'suggestions': _users}
275 277
276 278 @LoginRequired()
277 279 @XHRRequired()
278 280 @jsonify
279 281 def user_group_autocomplete_data(self):
280 282 query = request.GET.get('query')
281 283 active = str2bool(request.GET.get('active') or True)
282 284
283 285 repo_model = RepoModel()
284 286 _user_groups = repo_model.get_user_groups(
285 287 name_contains=query, only_active=active)
286 288 _user_groups = _user_groups
287 289
288 290 return {'suggestions': _user_groups}
@@ -1,55 +1,57 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 25 import importlib
26 26 import logging
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30 # leave defaults for backward compat
31 31 default_searcher = 'rhodecode.lib.index.whoosh'
32 32 default_location = '%(here)s/data/index'
33 33
34 34
35 35 class BaseSearch(object):
36 36 def __init__(self):
37 37 pass
38 38
39 39 def cleanup(self):
40 40 pass
41 41
42 def search(self, query, document_type, search_user, repo_name=None):
42 def search(self, query, document_type, search_user, repo_name=None,
43 raise_on_exc=True):
43 44 raise Exception('NotImplemented')
44 45
46
45 47 def searcher_from_config(config, prefix='search.'):
46 48 _config = {}
47 49 for key in config.keys():
48 50 if key.startswith(prefix):
49 51 _config[key[len(prefix):]] = config[key]
50 52
51 53 if 'location' not in _config:
52 54 _config['location'] = default_location
53 55 imported = importlib.import_module(_config.get('module', default_searcher))
54 56 searcher = imported.Search(config=_config)
55 57 return searcher
@@ -1,279 +1,280 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 25 from __future__ import absolute_import
26 26 import logging
27 27 import os
28 28 import re
29 29
30 30 from pylons.i18n.translation import _
31 31
32 32 from whoosh import query as query_lib, sorting
33 33 from whoosh.highlight import HtmlFormatter, ContextFragmenter
34 34 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
35 35 from whoosh.qparser import QueryParser, QueryParserError
36 36
37 37 import rhodecode.lib.helpers as h
38 38 from rhodecode.lib.index import BaseSearch
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 try:
44 44 # we first try to import from rhodecode tools, fallback to copies if
45 45 # we're unable to
46 46 from rhodecode_tools.lib.fts_index.whoosh_schema import (
47 47 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
48 48 COMMIT_SCHEMA)
49 49 except ImportError:
50 50 log.warning('rhodecode_tools schema not available, doing a fallback '
51 51 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
52 52 from rhodecode.lib.index.whoosh_fallback_schema import (
53 53 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
54 54 COMMIT_SCHEMA)
55 55
56 56
57 57 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
58 58 FRAGMENTER = ContextFragmenter(200)
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63
64 63 class Search(BaseSearch):
65 64
66 65 name = 'whoosh'
67 66
68 67 def __init__(self, config):
68 super(Search, self).__init__()
69 69 self.config = config
70 70 if not os.path.isdir(self.config['location']):
71 71 os.makedirs(self.config['location'])
72 72
73 73 opener = create_in
74 74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 75 opener = open_dir
76 76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 77 indexname=FILE_INDEX_NAME)
78 78
79 79 opener = create_in
80 80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 81 opener = open_dir
82 82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 83 indexname=COMMIT_INDEX_NAME)
84 84
85 85 self.commit_schema = COMMIT_SCHEMA
86 86 self.commit_index = changeset_index
87 87 self.file_schema = FILE_SCHEMA
88 88 self.file_index = file_index
89 89 self.searcher = None
90 90
91 91 def cleanup(self):
92 92 if self.searcher:
93 93 self.searcher.close()
94 94
95 95 def _extend_query(self, query):
96 96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 97 if hashes:
98 98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 100 return query
101 101
102 def search(self, query, document_type, search_user, repo_name=None,
103 requested_page=1, page_limit=10, sort=None):
102 def search(self, query, document_type, search_user,
103 repo_name=None, requested_page=1, page_limit=10, sort=None,
104 raise_on_exc=True):
104 105
105 106 original_query = query
106 107 query = self._extend_query(query)
107 108
108 109 log.debug(u'QUERY: %s on %s', query, document_type)
109 110 result = {
110 111 'results': [],
111 112 'count': 0,
112 113 'error': None,
113 114 'runtime': 0
114 115 }
115 116 search_type, index_name, schema_defn = self._prepare_for_search(
116 117 document_type)
117 118 self._init_searcher(index_name)
118 119 try:
119 120 qp = QueryParser(search_type, schema=schema_defn)
120 121 allowed_repos_filter = self._get_repo_filter(
121 122 search_user, repo_name)
122 123 try:
123 124 query = qp.parse(unicode(query))
124 125 log.debug('query: %s (%s)' % (query, repr(query)))
125 126
126 127 reverse, sortedby = False, None
127 128 if search_type == 'message':
128 129 if sort == 'oldfirst':
129 130 sortedby = 'date'
130 131 reverse = False
131 132 elif sort == 'newfirst':
132 133 sortedby = 'date'
133 134 reverse = True
134 135
135 136 whoosh_results = self.searcher.search(
136 137 query, filter=allowed_repos_filter, limit=None,
137 138 sortedby=sortedby, reverse=reverse)
138 139
139 140 # fixes for 32k limit that whoosh uses for highlight
140 141 whoosh_results.fragmenter.charlimit = None
141 142 res_ln = whoosh_results.scored_length()
142 143 result['runtime'] = whoosh_results.runtime
143 144 result['count'] = res_ln
144 145 result['results'] = WhooshResultWrapper(
145 146 search_type, res_ln, whoosh_results)
146 147
147 148 except QueryParserError:
148 149 result['error'] = _('Invalid search query. Try quoting it.')
149 150 except (EmptyIndexError, IOError, OSError):
150 151 msg = _('There is no index to search in. '
151 152 'Please run whoosh indexer')
152 153 log.exception(msg)
153 154 result['error'] = msg
154 155 except Exception:
155 156 msg = _('An error occurred during this search operation')
156 157 log.exception(msg)
157 158 result['error'] = msg
158 159
159 160 return result
160 161
161 162 def statistics(self):
162 163 stats = [
163 164 {'key': _('Index Type'), 'value': 'Whoosh'},
164 165 {'key': _('File Index'), 'value': str(self.file_index)},
165 166 {'key': _('Indexed documents'),
166 167 'value': self.file_index.doc_count()},
167 168 {'key': _('Last update'),
168 169 'value': h.time_to_datetime(self.file_index.last_modified())},
169 170 {'key': _('Commit index'), 'value': str(self.commit_index)},
170 171 {'key': _('Indexed documents'),
171 172 'value': str(self.commit_index.doc_count())},
172 173 {'key': _('Last update'),
173 174 'value': h.time_to_datetime(self.commit_index.last_modified())}
174 175 ]
175 176 return stats
176 177
177 178 def _get_repo_filter(self, auth_user, repo_name):
178 179
179 180 allowed_to_search = [
180 181 repo for repo, perm in
181 182 auth_user.permissions['repositories'].items()
182 183 if perm != 'repository.none']
183 184
184 185 if repo_name:
185 186 repo_filter = [query_lib.Term('repository', repo_name)]
186 187
187 188 elif 'hg.admin' in auth_user.permissions.get('global', []):
188 189 return None
189 190
190 191 else:
191 192 repo_filter = [query_lib.Term('repository', _rn)
192 193 for _rn in allowed_to_search]
193 194 # in case we're not allowed to search anywhere, it's a trick
194 195 # to tell whoosh we're filtering, on ALL results
195 196 repo_filter = repo_filter or [query_lib.Term('repository', '')]
196 197
197 198 return query_lib.Or(repo_filter)
198 199
199 200 def _prepare_for_search(self, cur_type):
200 201 search_type = {
201 202 'content': 'content',
202 203 'commit': 'message',
203 204 'path': 'path',
204 205 'repository': 'repository'
205 206 }.get(cur_type, 'content')
206 207
207 208 index_name = {
208 209 'content': FILE_INDEX_NAME,
209 210 'commit': COMMIT_INDEX_NAME,
210 211 'path': FILE_INDEX_NAME
211 212 }.get(cur_type, FILE_INDEX_NAME)
212 213
213 214 schema_defn = {
214 215 'content': self.file_schema,
215 216 'commit': self.commit_schema,
216 217 'path': self.file_schema
217 218 }.get(cur_type, self.file_schema)
218 219
219 220 log.debug('IDX: %s' % index_name)
220 221 log.debug('SCHEMA: %s' % schema_defn)
221 222 return search_type, index_name, schema_defn
222 223
223 224 def _init_searcher(self, index_name):
224 225 idx = open_dir(self.config['location'], indexname=index_name)
225 226 self.searcher = idx.searcher()
226 227 return self.searcher
227 228
228 229
229 230 class WhooshResultWrapper(object):
230 231 def __init__(self, search_type, total_hits, results):
231 232 self.search_type = search_type
232 233 self.results = results
233 234 self.total_hits = total_hits
234 235
235 236 def __str__(self):
236 237 return '<%s at %s>' % (self.__class__.__name__, len(self))
237 238
238 239 def __repr__(self):
239 240 return self.__str__()
240 241
241 242 def __len__(self):
242 243 return self.total_hits
243 244
244 245 def __iter__(self):
245 246 """
246 247 Allows Iteration over results,and lazy generate content
247 248
248 249 *Requires* implementation of ``__getitem__`` method.
249 250 """
250 251 for hit in self.results:
251 252 yield self.get_full_content(hit)
252 253
253 254 def __getitem__(self, key):
254 255 """
255 256 Slicing of resultWrapper
256 257 """
257 258 i, j = key.start, key.stop
258 259 for hit in self.results[i:j]:
259 260 yield self.get_full_content(hit)
260 261
261 262 def get_full_content(self, hit):
262 263 # TODO: marcink: this feels like an overkill, there's a lot of data
263 264 # inside hit object, and we don't need all
264 265 res = dict(hit)
265 266
266 267 f_path = '' # noqa
267 268 if self.search_type in ['content', 'path']:
268 269 f_path = res['path'][len(res['repository']):]
269 270 f_path = f_path.lstrip(os.sep)
270 271
271 272 if self.search_type == 'content':
272 273 res.update({'content_short_hl': hit.highlights('content'),
273 274 'f_path': f_path})
274 275 elif self.search_type == 'path':
275 276 res.update({'f_path': f_path})
276 277 elif self.search_type == 'message':
277 278 res.update({'message_hl': hit.highlights('message')})
278 279
279 280 return res
General Comments 0
You need to be logged in to leave comments. Login now