blob: 7fe2918d493de8cbbf339408218f708be6b8741f [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Classes to hold information parsed from a request.
7
8To simplify our servlets and avoid duplication of code, we parse some
9info out of the request as soon as we get it and then pass a MonorailRequest
10object to the servlet-specific request handler methods.
11"""
12from __future__ import print_function
13from __future__ import division
14from __future__ import absolute_import
15
16import endpoints
17import logging
18import re
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020019from six.moves import urllib
Copybara854996b2021-09-07 19:36:02 +000020
21import ezt
22import six
23
24from google.appengine.api import app_identity
25from google.appengine.api import oauth
26
27import webapp2
28
29import settings
30from businesslogic import work_env
31from features import features_constants
32from framework import authdata
33from framework import exceptions
34from framework import framework_bizobj
35from framework import framework_constants
36from framework import framework_views
37from framework import monorailcontext
38from framework import permissions
39from framework import profiler
40from framework import sql
41from framework import template_helpers
42from proto import api_pb2_v1
43from tracker import tracker_bizobj
44from tracker import tracker_constants
45
46
47_HOSTPORT_RE = re.compile('^[-a-z0-9.]+(:\d+)?$', re.I)
48
49
50# TODO(jrobbins): Stop extending MonorailContext and change whole servlet
51# framework to pass around separate objects for mc and mr.
52class MonorailRequestBase(monorailcontext.MonorailContext):
53 """A base class with common attributes for internal and external requests."""
54
55 def __init__(self, services, requester=None, cnxn=None):
56 super(MonorailRequestBase, self).__init__(
57 services, cnxn=cnxn, requester=requester)
58
59 self.project_name = None
60 self.project = None
61 self.config = None
62
63 @property
64 def project_id(self):
65 return self.project.project_id if self.project else None
66
67
68class MonorailApiRequest(MonorailRequestBase):
69 """A class to hold information parsed from the Endpoints API request."""
70
71 # pylint: disable=attribute-defined-outside-init
72 def __init__(self, request, services, cnxn=None):
73 requester_object = (
74 endpoints.get_current_user() or
75 oauth.get_current_user(
76 framework_constants.OAUTH_SCOPE))
77 requester = requester_object.email().lower()
78 super(MonorailApiRequest, self).__init__(
79 services, requester=requester, cnxn=cnxn)
80 self.me_user_id = self.auth.user_id
81 self.viewed_username = None
82 self.viewed_user_auth = None
83 self.issue = None
84 self.granted_perms = set()
85
86 # query parameters
87 self.params = {
88 'can': 1,
89 'start': 0,
90 'num': tracker_constants.DEFAULT_RESULTS_PER_PAGE,
91 'q': '',
92 'sort': '',
93 'groupby': '',
94 'projects': [],
95 'hotlists': []
96 }
97 self.use_cached_searches = True
98 self.mode = None
99
100 if hasattr(request, 'projectId'):
101 self.project_name = request.projectId
102 with work_env.WorkEnv(self, services) as we:
103 self.project = we.GetProjectByName(self.project_name)
104 self.params['projects'].append(self.project_name)
105 self.config = we.GetProjectConfig(self.project_id)
106 if hasattr(request, 'additionalProject'):
107 self.params['projects'].extend(request.additionalProject)
108 self.params['projects'] = list(set(self.params['projects']))
109 self.LookupLoggedInUserPerms(self.project)
110 if hasattr(request, 'projectId'):
111 with work_env.WorkEnv(self, services) as we:
112 if hasattr(request, 'issueId'):
113 self.issue = we.GetIssueByLocalID(
114 self.project_id, request.issueId, use_cache=False)
115 self.granted_perms = tracker_bizobj.GetGrantedPerms(
116 self.issue, self.auth.effective_ids, self.config)
117 if hasattr(request, 'userId'):
118 self.viewed_username = request.userId.lower()
119 if self.viewed_username == 'me':
120 self.viewed_username = requester
121 self.viewed_user_auth = authdata.AuthData.FromEmail(
122 self.cnxn, self.viewed_username, services)
123 elif hasattr(request, 'groupName'):
124 self.viewed_username = request.groupName.lower()
125 try:
126 self.viewed_user_auth = authdata.AuthData.FromEmail(
127 self.cnxn, self.viewed_username, services)
128 except exceptions.NoSuchUserException:
129 self.viewed_user_auth = None
130
131 # Build q.
132 if hasattr(request, 'q') and request.q:
133 self.params['q'] = request.q
134 if hasattr(request, 'publishedMax') and request.publishedMax:
135 self.params['q'] += ' opened<=%d' % request.publishedMax
136 if hasattr(request, 'publishedMin') and request.publishedMin:
137 self.params['q'] += ' opened>=%d' % request.publishedMin
138 if hasattr(request, 'updatedMax') and request.updatedMax:
139 self.params['q'] += ' modified<=%d' % request.updatedMax
140 if hasattr(request, 'updatedMin') and request.updatedMin:
141 self.params['q'] += ' modified>=%d' % request.updatedMin
142 if hasattr(request, 'owner') and request.owner:
143 self.params['q'] += ' owner:%s' % request.owner
144 if hasattr(request, 'status') and request.status:
145 self.params['q'] += ' status:%s' % request.status
146 if hasattr(request, 'label') and request.label:
147 self.params['q'] += ' label:%s' % request.label
148
149 if hasattr(request, 'can') and request.can:
150 if request.can == api_pb2_v1.CannedQuery.all:
151 self.params['can'] = 1
152 elif request.can == api_pb2_v1.CannedQuery.new:
153 self.params['can'] = 6
154 elif request.can == api_pb2_v1.CannedQuery.open:
155 self.params['can'] = 2
156 elif request.can == api_pb2_v1.CannedQuery.owned:
157 self.params['can'] = 3
158 elif request.can == api_pb2_v1.CannedQuery.reported:
159 self.params['can'] = 4
160 elif request.can == api_pb2_v1.CannedQuery.starred:
161 self.params['can'] = 5
162 elif request.can == api_pb2_v1.CannedQuery.to_verify:
163 self.params['can'] = 7
164 else: # Endpoints should have caught this.
165 raise exceptions.InputException(
166 'Canned query %s is not supported.', request.can)
167 if hasattr(request, 'startIndex') and request.startIndex:
168 self.params['start'] = request.startIndex
169 if hasattr(request, 'maxResults') and request.maxResults:
170 self.params['num'] = request.maxResults
171 if hasattr(request, 'sort') and request.sort:
172 self.params['sort'] = request.sort
173
174 self.query_project_names = self.GetParam('projects')
175 self.group_by_spec = self.GetParam('groupby')
176 self.group_by_spec = ' '.join(ParseColSpec(
177 self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
178 self.sort_spec = self.GetParam('sort')
179 self.sort_spec = ' '.join(ParseColSpec(self.sort_spec))
180 self.query = self.GetParam('q')
181 self.can = self.GetParam('can')
182 self.start = self.GetParam('start')
183 self.num = self.GetParam('num')
184
185 def GetParam(self, query_param_name, default_value=None,
186 _antitamper_re=None):
187 return self.params.get(query_param_name, default_value)
188
189 def GetPositiveIntParam(self, query_param_name, default_value=None):
190 """Returns 0 if the user-provided value is less than 0."""
191 return max(self.GetParam(query_param_name, default_value=default_value),
192 0)
193
194
195class MonorailRequest(MonorailRequestBase):
196 """A class to hold information parsed from the HTTP request.
197
198 The goal of MonorailRequest is to do almost all URL path and query string
199 procesing in one place, which makes the servlet code simpler.
200
201 Attributes:
202 cnxn: connection to the SQL databases.
203 logged_in_user_id: int user ID of the signed-in user, or None.
204 effective_ids: set of signed-in user ID and all their user group IDs.
205 user_pb: User object for the signed in user.
206 project_name: string name of the current project.
207 project_id: int ID of the current projet.
208 viewed_username: string username of the user whose profile is being viewed.
209 can: int "canned query" number to scope the user's search.
210 num: int number of results to show per pagination page.
211 start: int position in result set to show on this pagination page.
212 etc: there are many more, all read-only.
213 """
214
215 # pylint: disable=attribute-defined-outside-init
216 def __init__(self, services, params=None):
217 """Initialize the MonorailRequest object."""
218 # Note: mr starts off assuming anon until ParseRequest() is called.
219 super(MonorailRequest, self).__init__(services)
220 self.form_overrides = {}
221 if params:
222 self.form_overrides.update(params)
223 self.debug_enabled = False
224 self.use_cached_searches = True
225
226 self.hotlist_id = None
227 self.hotlist = None
228 self.hotlist_name = None
229
230 self.viewed_username = None
231 self.viewed_user_auth = authdata.AuthData()
232
233 def ParseRequest(self, request, services, do_user_lookups=True):
234 """Parse tons of useful info from the given request object.
235
236 Args:
237 request: webapp2 Request object w/ path and query params.
238 services: connections to backend servers including DB.
239 do_user_lookups: Set to False to disable lookups during testing.
240 """
241 with self.profiler.Phase('basic parsing'):
242 self.request = request
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200243 self.request_path = request.path
Copybara854996b2021-09-07 19:36:02 +0000244 self.current_page_url = request.url
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200245 self.current_page_url_encoded = urllib.parse.quote_plus(
246 self.current_page_url)
Copybara854996b2021-09-07 19:36:02 +0000247
248 # Only accept a hostport from the request that looks valid.
249 if not _HOSTPORT_RE.match(request.host):
250 raise exceptions.InputException(
251 'request.host looks funny: %r', request.host)
252
253 logging.info('Request: %s', self.current_page_url)
254
255 with self.profiler.Phase('path parsing'):
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200256 (viewed_user_val, self.project_name, self.hotlist_id,
257 self.hotlist_name) = _ParsePathIdentifiers(self.request_path)
Copybara854996b2021-09-07 19:36:02 +0000258 self.viewed_username = _GetViewedEmail(
259 viewed_user_val, self.cnxn, services)
260 with self.profiler.Phase('qs parsing'):
261 self._ParseQueryParameters()
262 with self.profiler.Phase('overrides parsing'):
263 self._ParseFormOverrides()
264
265 if not self.project: # It can be already set in unit tests.
266 self._LookupProject(services)
267 if self.project_id and services.config:
268 self.config = services.config.GetProjectConfig(self.cnxn, self.project_id)
269
270 if do_user_lookups:
271 if self.viewed_username:
272 self._LookupViewedUser(services)
273 self._LookupLoggedInUser(services)
Copybara854996b2021-09-07 19:36:02 +0000274
275 if not self.hotlist:
276 self._LookupHotlist(services)
277
278 if self.query is None:
279 self.query = self._CalcDefaultQuery()
280
281 prod_debug_allowed = self.perms.HasPerm(
282 permissions.VIEW_DEBUG, self.auth.user_id, None)
283 self.debug_enabled = (request.params.get('debug') and
284 (settings.local_mode or prod_debug_allowed))
285 # temporary option for perf testing on staging instance.
286 if request.params.get('disable_cache'):
287 if settings.local_mode or 'staging' in request.host:
288 self.use_cached_searches = False
289
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200290 def ParseFlaskRequest(self, request, services, do_user_lookups=True):
291 """Parse tons of useful info from the given flask request object.
292
293 Args:
294 request: flask Request object w/ path and query params.
295 services: connections to backend servers including DB.
296 do_user_lookups: Set to False to disable lookups during testing.
297 """
298 with self.profiler.Phase('basic parsing'):
299 self.request = request
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200300 self.request_path = request.base_url[len(request.host_url) - 1:]
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200301 self.current_page_url = request.url
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200302 self.current_page_url_encoded = urllib.parse.quote_plus(
303 self.current_page_url)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200304
305 # Only accept a hostport from the request that looks valid.
306 if not _HOSTPORT_RE.match(request.host):
307 raise exceptions.InputException(
308 'request.host looks funny: %r', request.host)
309
310 logging.info('Flask Request: %s', self.current_page_url)
311
312 with self.profiler.Phase('path parsing'):
313 (viewed_user_val, self.project_name, self.hotlist_id,
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200314 self.hotlist_name) = _ParsePathIdentifiers(self.request_path)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200315 self.viewed_username = _GetViewedEmail(
316 viewed_user_val, self.cnxn, services)
317 with self.profiler.Phase('qs parsing'):
318 self._ParseQueryParameters()
319 with self.profiler.Phase('overrides parsing'):
320 self._ParseFormOverrides()
321
322 if not self.project: # It can be already set in unit tests.
323 self._LookupProject(services)
324 if self.project_id and services.config:
325 self.config = services.config.GetProjectConfig(self.cnxn, self.project_id)
326
327 if do_user_lookups:
328 if self.viewed_username:
329 self._LookupViewedUser(services)
330 self._LookupLoggedInUser(services)
331
332 if not self.hotlist:
333 self._LookupHotlist(services)
334
335 if self.query is None:
336 self.query = self._CalcDefaultQuery()
337
338 prod_debug_allowed = self.perms.HasPerm(
339 permissions.VIEW_DEBUG, self.auth.user_id, None)
340 self.debug_enabled = (
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200341 request.values.get('debug') and
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200342 (settings.local_mode or prod_debug_allowed))
343 # temporary option for perf testing on staging instance.
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200344 if request.values.get('disable_cache'):
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200345 if settings.local_mode or 'staging' in request.host:
346 self.use_cached_searches = False
347
Copybara854996b2021-09-07 19:36:02 +0000348 def _CalcDefaultQuery(self):
349 """When URL has no q= param, return the default for members or ''."""
350 if (self.can == 2 and self.project and self.auth.effective_ids and
351 framework_bizobj.UserIsInProject(self.project, self.auth.effective_ids)
352 and self.config):
353 return self.config.member_default_query
354 else:
355 return ''
356
357 def _ParseQueryParameters(self):
358 """Parse and convert all the query string params used in any servlet."""
359 self.start = self.GetPositiveIntParam('start', default_value=0)
360 self.num = self.GetPositiveIntParam(
361 'num', default_value=tracker_constants.DEFAULT_RESULTS_PER_PAGE)
362 # Prevent DoS attacks that try to make us serve really huge result pages.
363 self.num = min(self.num, settings.max_artifact_search_results_per_page)
364
365 self.invalidation_timestep = self.GetIntParam(
366 'invalidation_timestep', default_value=0)
367
368 self.continue_issue_id = self.GetIntParam(
369 'continue_issue_id', default_value=0)
370 self.redir = self.GetParam('redir')
371
372 # Search scope, a.k.a., canned query ID
373 # TODO(jrobbins): make configurable
374 self.can = self.GetIntParam(
375 'can', default_value=tracker_constants.OPEN_ISSUES_CAN)
376
377 # Search query
378 self.query = self.GetParam('q')
379
380 # Sorting of search results (needed for result list and flipper)
381 self.sort_spec = self.GetParam(
382 'sort', default_value='',
383 antitamper_re=framework_constants.SORTSPEC_RE)
384 self.sort_spec = ' '.join(ParseColSpec(self.sort_spec))
385
386 # Note: This is set later in request handling by ComputeColSpec().
387 self.col_spec = None
388
389 # Grouping of search results (needed for result list and flipper)
390 self.group_by_spec = self.GetParam(
391 'groupby', default_value='',
392 antitamper_re=framework_constants.SORTSPEC_RE)
393 self.group_by_spec = ' '.join(ParseColSpec(
394 self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
395
396 # For issue list and grid mode.
397 self.cursor = self.GetParam('cursor')
398 self.preview = self.GetParam('preview')
399 self.mode = self.GetParam('mode') or 'list'
400 self.x = self.GetParam('x', default_value='')
401 self.y = self.GetParam('y', default_value='')
402 self.cells = self.GetParam('cells', default_value='ids')
403
404 # For the dashboard and issue lists included in the dashboard.
405 self.ajah = self.GetParam('ajah') # AJAH = Asychronous Javascript And HTML
406 self.table_title = self.GetParam('table_title')
407 self.panel_id = self.GetIntParam('panel')
408
409 # For pagination of updates lists
410 self.before = self.GetPositiveIntParam('before')
411 self.after = self.GetPositiveIntParam('after')
412
413 # For cron tasks and backend calls
414 self.lower_bound = self.GetIntParam('lower_bound')
415 self.upper_bound = self.GetIntParam('upper_bound')
416 self.shard_id = self.GetIntParam('shard_id')
417
418 # For specifying which objects to operate on
419 self.local_id = self.GetIntParam('id')
420 self.local_id_list = self.GetIntListParam('ids')
421 self.seq = self.GetIntParam('seq')
422 self.aid = self.GetIntParam('aid')
423 self.signed_aid = self.GetParam('signed_aid')
424 self.specified_user_id = self.GetIntParam('u', default_value=0)
425 self.specified_logged_in_user_id = self.GetIntParam(
426 'logged_in_user_id', default_value=0)
427 self.specified_me_user_ids = self.GetIntListParam('me_user_ids')
428
429 # TODO(jrobbins): Phase this out after next deployment. If an old
430 # version of the default GAE module sends a request with the old
431 # me_user_id= parameter, then accept it.
432 specified_me_user_id = self.GetIntParam(
433 'me_user_id', default_value=0)
434 if specified_me_user_id:
435 self.specified_me_user_ids = [specified_me_user_id]
436
437 self.specified_project = self.GetParam('project')
438 self.specified_project_id = self.GetIntParam('project_id')
439 self.query_project_names = self.GetListParam('projects', default_value=[])
440 self.template_name = self.GetParam('template')
441 self.component_path = self.GetParam('component')
442 self.field_name = self.GetParam('field')
443
444 # For image attachments
445 self.inline = bool(self.GetParam('inline'))
446 self.thumb = bool(self.GetParam('thumb'))
447
448 # For JS callbacks
449 self.token = self.GetParam('token')
450 self.starred = bool(self.GetIntParam('starred'))
451
452 # For issue reindexing utility servlet
453 self.auto_submit = self.GetParam('auto_submit')
454
455 # For issue dependency reranking servlet
456 self.parent_id = self.GetIntParam('parent_id')
457 self.target_id = self.GetIntParam('target_id')
458 self.moved_ids = self.GetIntListParam('moved_ids')
459 self.split_above = self.GetBoolParam('split_above')
460
461 # For adding issues to hotlists servlet
462 self.hotlist_ids_remove = self.GetIntListParam('hotlist_ids_remove')
463 self.hotlist_ids_add = self.GetIntListParam('hotlist_ids_add')
464 self.issue_refs = self.GetListParam('issue_refs')
465
466 def _ParseFormOverrides(self):
467 """Support deep linking by allowing the user to set form fields via QS."""
468 allowed_overrides = {
469 'template_name': self.GetParam('template_name'),
470 'initial_summary': self.GetParam('summary'),
471 'initial_description': (self.GetParam('description') or
472 self.GetParam('comment')),
473 'initial_comment': self.GetParam('comment'),
474 'initial_status': self.GetParam('status'),
475 'initial_owner': self.GetParam('owner'),
476 'initial_cc': self.GetParam('cc'),
477 'initial_blocked_on': self.GetParam('blockedon'),
478 'initial_blocking': self.GetParam('blocking'),
479 'initial_merge_into': self.GetIntParam('mergeinto'),
480 'initial_components': self.GetParam('components'),
481 'initial_hotlists': self.GetParam('hotlists'),
482
483 # For the people pages
484 'initial_add_members': self.GetParam('add_members'),
485 'initially_expanded_form': ezt.boolean(self.GetParam('expand_form')),
486
487 # For user group admin pages
488 'initial_name': (self.GetParam('group_name') or
489 self.GetParam('proposed_project_name')),
490 }
491
492 # Only keep the overrides that were actually provided in the query string.
493 self.form_overrides.update(
494 (k, v) for (k, v) in allowed_overrides.items()
495 if v is not None)
496
497 def _LookupViewedUser(self, services):
498 """Get information about the viewed user (if any) from the request."""
499 try:
500 with self.profiler.Phase('get viewed user, if any'):
501 self.viewed_user_auth = authdata.AuthData.FromEmail(
502 self.cnxn, self.viewed_username, services, autocreate=False)
503 except exceptions.NoSuchUserException:
504 logging.info('could not find user %r', self.viewed_username)
505 webapp2.abort(404, 'user not found')
506
507 if not self.viewed_user_auth.user_id:
508 webapp2.abort(404, 'user not found')
509
510 def _LookupProject(self, services):
511 """Get information about the current project (if any) from the request.
512
513 Raises:
514 NoSuchProjectException if there is no project with that name.
515 """
516 logging.info('project_name is %r', self.project_name)
517 if self.project_name:
518 self.project = services.project.GetProjectByName(
519 self.cnxn, self.project_name)
520 if not self.project:
521 raise exceptions.NoSuchProjectException()
522
523 def _LookupHotlist(self, services):
524 """Get information about the current hotlist (if any) from the request."""
525 with self.profiler.Phase('get current hotlist, if any'):
526 if self.hotlist_name:
527 hotlist_id_dict = services.features.LookupHotlistIDs(
528 self.cnxn, [self.hotlist_name], [self.viewed_user_auth.user_id])
529 try:
530 self.hotlist_id = hotlist_id_dict[(
531 self.hotlist_name, self.viewed_user_auth.user_id)]
532 except KeyError:
533 webapp2.abort(404, 'invalid hotlist')
534
535 if not self.hotlist_id:
536 logging.info('no hotlist_id or bad hotlist_name, so no hotlist')
537 else:
538 self.hotlist = services.features.GetHotlistByID(
539 self.cnxn, self.hotlist_id)
540 if not self.hotlist or (
541 self.viewed_user_auth.user_id and
542 self.viewed_user_auth.user_id not in self.hotlist.owner_ids):
543 webapp2.abort(404, 'invalid hotlist')
544
545 def _LookupLoggedInUser(self, services):
546 """Get information about the signed-in user (if any) from the request."""
547 self.auth = authdata.AuthData.FromRequest(self.cnxn, services)
548 self.me_user_id = (self.GetIntParam('me') or
549 self.viewed_user_auth.user_id or self.auth.user_id)
550
551 self.LookupLoggedInUserPerms(self.project)
552
553 def ComputeColSpec(self, config):
554 """Set col_spec based on param, default in the config, or site default."""
555 if self.col_spec is not None:
556 return # Already set.
557 default_col_spec = ''
558 if config:
559 default_col_spec = config.default_col_spec
560
561 col_spec = self.GetParam(
562 'colspec', default_value=default_col_spec,
563 antitamper_re=framework_constants.COLSPEC_RE)
564 cols_lower = col_spec.lower().split()
565 if self.project and any(
566 hotlist_col in cols_lower for hotlist_col in [
567 'rank', 'adder', 'added']):
568 # if the the list is a project list and the 'colspec' is a carry-over
569 # from hotlists, set col_spec to None so it will be set to default in
570 # in the next if statement
571 col_spec = None
572
573 if not col_spec:
574 # If col spec is still empty then default to the global col spec.
575 col_spec = tracker_constants.DEFAULT_COL_SPEC
576
577 self.col_spec = ' '.join(ParseColSpec(col_spec,
578 max_parts=framework_constants.MAX_COL_PARTS))
579
580 def PrepareForReentry(self, echo_data):
581 """Expose the results of form processing as if it was a new GET.
582
583 This method is called only when the user submits a form with invalid
584 information which they are being asked to correct it. Updating the MR
585 object allows the normal servlet get() method to populate the form with
586 the entered values and error messages.
587
588 Args:
589 echo_data: dict of {page_data_key: value_to_reoffer, ...} that will
590 override whatever HTML form values are nomally shown to the
591 user when they initially view the form. This allows them to
592 fix user input that was not valid.
593 """
594 self.form_overrides.update(echo_data)
595
596 def GetParam(self, query_param_name, default_value=None,
597 antitamper_re=None):
598 """Get a query parameter from the URL as a utf8 string."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200599 value = None
600 if hasattr(self.request, 'params'):
601 value = self.request.params.get(query_param_name)
602 else:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200603 value = self.request.values.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000604 assert value is None or isinstance(value, six.text_type)
605 using_default = value is None
606 if using_default:
607 value = default_value
608
609 if antitamper_re and not antitamper_re.match(value):
610 if using_default:
611 logging.error('Default value fails antitamper for %s field: %s',
612 query_param_name, value)
613 else:
614 logging.info('User seems to have tampered with %s field: %s',
615 query_param_name, value)
616 raise exceptions.InputException()
617
618 return value
619
620 def GetIntParam(self, query_param_name, default_value=None):
621 """Get an integer param from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200622 value = None
623 if hasattr(self.request, 'params'):
624 value = self.request.params.get(query_param_name)
625 else:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200626 value = self.request.values.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000627 if value is None or value == '':
628 return default_value
629
630 try:
631 return int(value)
632 except (TypeError, ValueError):
633 raise exceptions.InputException(
634 'Invalid value for integer param: %r' % value)
635
636 def GetPositiveIntParam(self, query_param_name, default_value=None):
637 """Returns 0 if the user-provided value is less than 0."""
638 return max(self.GetIntParam(query_param_name, default_value=default_value),
639 0)
640
641 def GetListParam(self, query_param_name, default_value=None):
642 """Get a list of strings from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200643 params = None
644 if hasattr(self.request, 'params'):
645 params = self.request.params.get(query_param_name)
646 else:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200647 params = self.request.values.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000648 if params is None:
649 return default_value
650 if not params:
651 return []
652 return params.split(',')
653
654 def GetIntListParam(self, query_param_name, default_value=None):
655 """Get a list of ints from the URL or default."""
656 param_list = self.GetListParam(query_param_name)
657 if param_list is None:
658 return default_value
659
660 try:
661 return [int(p) for p in param_list]
662 except (TypeError, ValueError):
663 raise exceptions.InputException('Invalid value for integer list param')
664
665 def GetBoolParam(self, query_param_name, default_value=None):
666 """Get a boolean param from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200667 value = None
668 if hasattr(self.request, 'params'):
669 value = self.request.params.get(query_param_name)
670 else:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200671 value = self.request.values.get(query_param_name)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200672
Copybara854996b2021-09-07 19:36:02 +0000673 if value is None:
674 return default_value
675
676 if (not value) or (value.lower() == 'false'):
677 return False
678 return True
679
680
681def _ParsePathIdentifiers(path):
682 """Parse out the workspace being requested (if any).
683
684 Args:
685 path: A string beginning with the request's path info.
686
687 Returns:
688 (viewed_user_val, project_name).
689 """
690 viewed_user_val = None
691 project_name = None
692 hotlist_id = None
693 hotlist_name = None
694
695 # Strip off any query params
696 split_path = path.lstrip('/').split('?')[0].split('/')
697 if len(split_path) >= 2:
698 if split_path[0] == 'hotlists':
699 if split_path[1].isdigit():
700 hotlist_id = int(split_path[1])
701 if split_path[0] == 'p':
702 project_name = split_path[1]
703 if split_path[0] == 'u' or split_path[0] == 'users':
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200704 viewed_user_val = urllib.parse.unquote(split_path[1])
Copybara854996b2021-09-07 19:36:02 +0000705 if len(split_path) >= 4 and split_path[2] == 'hotlists':
706 try:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200707 hotlist_id = int(urllib.parse.unquote(split_path[3].split('.')[0]))
Copybara854996b2021-09-07 19:36:02 +0000708 except ValueError:
709 raw_last_path = (split_path[3][:-3] if
710 split_path[3].endswith('.do') else split_path[3])
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200711 last_path = urllib.parse.unquote(raw_last_path)
Copybara854996b2021-09-07 19:36:02 +0000712 match = framework_bizobj.RE_HOTLIST_NAME.match(
713 last_path)
714 if not match:
715 raise exceptions.InputException(
716 'Could not parse hotlist id or name')
717 else:
718 hotlist_name = last_path.lower()
719
720 if split_path[0] == 'g':
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200721 viewed_user_val = urllib.parse.unquote(split_path[1])
Copybara854996b2021-09-07 19:36:02 +0000722
723 return viewed_user_val, project_name, hotlist_id, hotlist_name
724
725
726def _GetViewedEmail(viewed_user_val, cnxn, services):
727 """Returns the viewed user's email.
728
729 Args:
730 viewed_user_val: Could be either int (user_id) or str (email).
731 cnxn: connection to the SQL database.
732 services: Interface to all persistence storage backends.
733
734 Returns:
735 viewed_email
736 """
737 if not viewed_user_val:
738 return None
739
740 try:
741 viewed_userid = int(viewed_user_val)
742 viewed_email = services.user.LookupUserEmail(cnxn, viewed_userid)
743 if not viewed_email:
744 logging.info('userID %s not found', viewed_userid)
745 webapp2.abort(404, 'user not found')
746 except ValueError:
747 viewed_email = viewed_user_val
748
749 return viewed_email
750
751
752def ParseColSpec(
753 col_spec, max_parts=framework_constants.MAX_SORT_PARTS,
754 ignore=None):
755 """Split a string column spec into a list of column names.
756
757 We dedup col parts because an attacker could try to DoS us or guess
758 zero or one result by measuring the time to process a request that
759 has a very long column list.
760
761 Args:
762 col_spec: a unicode string containing a list of labels.
763 max_parts: optional int maximum number of parts to consider.
764 ignore: optional list of column name parts to ignore.
765
766 Returns:
767 A list of the extracted labels. Non-alphanumeric
768 characters other than the period will be stripped from the text.
769 """
770 cols = framework_constants.COLSPEC_COL_RE.findall(col_spec)
771 result = [] # List of column headers with no duplicates.
772 # Set of column parts that we have processed so far.
773 seen = set()
774 if ignore:
775 seen = set(ignore_col.lower() for ignore_col in ignore)
776 max_parts += len(ignore)
777
778 for col in cols:
779 parts = []
780 for part in col.split('/'):
781 if (part.lower() not in seen and len(seen) < max_parts
782 and len(part) < framework_constants.MAX_COL_LEN):
783 parts.append(part)
784 seen.add(part.lower())
785 if parts:
786 result.append('/'.join(parts))
787 return result