blob: 94dd9d642e6a759c35255c9eaaec91d51b928008 [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
19import urllib
20
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
243 self.current_page_url = request.url
244 self.current_page_url_encoded = urllib.quote_plus(self.current_page_url)
245
246 # Only accept a hostport from the request that looks valid.
247 if not _HOSTPORT_RE.match(request.host):
248 raise exceptions.InputException(
249 'request.host looks funny: %r', request.host)
250
251 logging.info('Request: %s', self.current_page_url)
252
253 with self.profiler.Phase('path parsing'):
254 (viewed_user_val, self.project_name,
255 self.hotlist_id, self.hotlist_name) = _ParsePathIdentifiers(
256 self.request.path)
257 self.viewed_username = _GetViewedEmail(
258 viewed_user_val, self.cnxn, services)
259 with self.profiler.Phase('qs parsing'):
260 self._ParseQueryParameters()
261 with self.profiler.Phase('overrides parsing'):
262 self._ParseFormOverrides()
263
264 if not self.project: # It can be already set in unit tests.
265 self._LookupProject(services)
266 if self.project_id and services.config:
267 self.config = services.config.GetProjectConfig(self.cnxn, self.project_id)
268
269 if do_user_lookups:
270 if self.viewed_username:
271 self._LookupViewedUser(services)
272 self._LookupLoggedInUser(services)
Copybara854996b2021-09-07 19:36:02 +0000273
274 if not self.hotlist:
275 self._LookupHotlist(services)
276
277 if self.query is None:
278 self.query = self._CalcDefaultQuery()
279
280 prod_debug_allowed = self.perms.HasPerm(
281 permissions.VIEW_DEBUG, self.auth.user_id, None)
282 self.debug_enabled = (request.params.get('debug') and
283 (settings.local_mode or prod_debug_allowed))
284 # temporary option for perf testing on staging instance.
285 if request.params.get('disable_cache'):
286 if settings.local_mode or 'staging' in request.host:
287 self.use_cached_searches = False
288
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200289 def ParseFlaskRequest(self, request, services, do_user_lookups=True):
290 """Parse tons of useful info from the given flask request object.
291
292 Args:
293 request: flask Request object w/ path and query params.
294 services: connections to backend servers including DB.
295 do_user_lookups: Set to False to disable lookups during testing.
296 """
297 with self.profiler.Phase('basic parsing'):
298 self.request = request
299 self.current_page_url = request.url
300 self.current_page_url_encoded = urllib.quote_plus(self.current_page_url)
301
302 # Only accept a hostport from the request that looks valid.
303 if not _HOSTPORT_RE.match(request.host):
304 raise exceptions.InputException(
305 'request.host looks funny: %r', request.host)
306
307 logging.info('Flask Request: %s', self.current_page_url)
308
309 with self.profiler.Phase('path parsing'):
310 (viewed_user_val, self.project_name, self.hotlist_id,
311 self.hotlist_name) = _ParsePathIdentifiers(self.request.url)
312 self.viewed_username = _GetViewedEmail(
313 viewed_user_val, self.cnxn, services)
314 with self.profiler.Phase('qs parsing'):
315 self._ParseQueryParameters()
316 with self.profiler.Phase('overrides parsing'):
317 self._ParseFormOverrides()
318
319 if not self.project: # It can be already set in unit tests.
320 self._LookupProject(services)
321 if self.project_id and services.config:
322 self.config = services.config.GetProjectConfig(self.cnxn, self.project_id)
323
324 if do_user_lookups:
325 if self.viewed_username:
326 self._LookupViewedUser(services)
327 self._LookupLoggedInUser(services)
328
329 if not self.hotlist:
330 self._LookupHotlist(services)
331
332 if self.query is None:
333 self.query = self._CalcDefaultQuery()
334
335 prod_debug_allowed = self.perms.HasPerm(
336 permissions.VIEW_DEBUG, self.auth.user_id, None)
337 self.debug_enabled = (
338 request.args.get('debug') and
339 (settings.local_mode or prod_debug_allowed))
340 # temporary option for perf testing on staging instance.
341 if request.args.get('disable_cache'):
342 if settings.local_mode or 'staging' in request.host:
343 self.use_cached_searches = False
344
Copybara854996b2021-09-07 19:36:02 +0000345 def _CalcDefaultQuery(self):
346 """When URL has no q= param, return the default for members or ''."""
347 if (self.can == 2 and self.project and self.auth.effective_ids and
348 framework_bizobj.UserIsInProject(self.project, self.auth.effective_ids)
349 and self.config):
350 return self.config.member_default_query
351 else:
352 return ''
353
354 def _ParseQueryParameters(self):
355 """Parse and convert all the query string params used in any servlet."""
356 self.start = self.GetPositiveIntParam('start', default_value=0)
357 self.num = self.GetPositiveIntParam(
358 'num', default_value=tracker_constants.DEFAULT_RESULTS_PER_PAGE)
359 # Prevent DoS attacks that try to make us serve really huge result pages.
360 self.num = min(self.num, settings.max_artifact_search_results_per_page)
361
362 self.invalidation_timestep = self.GetIntParam(
363 'invalidation_timestep', default_value=0)
364
365 self.continue_issue_id = self.GetIntParam(
366 'continue_issue_id', default_value=0)
367 self.redir = self.GetParam('redir')
368
369 # Search scope, a.k.a., canned query ID
370 # TODO(jrobbins): make configurable
371 self.can = self.GetIntParam(
372 'can', default_value=tracker_constants.OPEN_ISSUES_CAN)
373
374 # Search query
375 self.query = self.GetParam('q')
376
377 # Sorting of search results (needed for result list and flipper)
378 self.sort_spec = self.GetParam(
379 'sort', default_value='',
380 antitamper_re=framework_constants.SORTSPEC_RE)
381 self.sort_spec = ' '.join(ParseColSpec(self.sort_spec))
382
383 # Note: This is set later in request handling by ComputeColSpec().
384 self.col_spec = None
385
386 # Grouping of search results (needed for result list and flipper)
387 self.group_by_spec = self.GetParam(
388 'groupby', default_value='',
389 antitamper_re=framework_constants.SORTSPEC_RE)
390 self.group_by_spec = ' '.join(ParseColSpec(
391 self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
392
393 # For issue list and grid mode.
394 self.cursor = self.GetParam('cursor')
395 self.preview = self.GetParam('preview')
396 self.mode = self.GetParam('mode') or 'list'
397 self.x = self.GetParam('x', default_value='')
398 self.y = self.GetParam('y', default_value='')
399 self.cells = self.GetParam('cells', default_value='ids')
400
401 # For the dashboard and issue lists included in the dashboard.
402 self.ajah = self.GetParam('ajah') # AJAH = Asychronous Javascript And HTML
403 self.table_title = self.GetParam('table_title')
404 self.panel_id = self.GetIntParam('panel')
405
406 # For pagination of updates lists
407 self.before = self.GetPositiveIntParam('before')
408 self.after = self.GetPositiveIntParam('after')
409
410 # For cron tasks and backend calls
411 self.lower_bound = self.GetIntParam('lower_bound')
412 self.upper_bound = self.GetIntParam('upper_bound')
413 self.shard_id = self.GetIntParam('shard_id')
414
415 # For specifying which objects to operate on
416 self.local_id = self.GetIntParam('id')
417 self.local_id_list = self.GetIntListParam('ids')
418 self.seq = self.GetIntParam('seq')
419 self.aid = self.GetIntParam('aid')
420 self.signed_aid = self.GetParam('signed_aid')
421 self.specified_user_id = self.GetIntParam('u', default_value=0)
422 self.specified_logged_in_user_id = self.GetIntParam(
423 'logged_in_user_id', default_value=0)
424 self.specified_me_user_ids = self.GetIntListParam('me_user_ids')
425
426 # TODO(jrobbins): Phase this out after next deployment. If an old
427 # version of the default GAE module sends a request with the old
428 # me_user_id= parameter, then accept it.
429 specified_me_user_id = self.GetIntParam(
430 'me_user_id', default_value=0)
431 if specified_me_user_id:
432 self.specified_me_user_ids = [specified_me_user_id]
433
434 self.specified_project = self.GetParam('project')
435 self.specified_project_id = self.GetIntParam('project_id')
436 self.query_project_names = self.GetListParam('projects', default_value=[])
437 self.template_name = self.GetParam('template')
438 self.component_path = self.GetParam('component')
439 self.field_name = self.GetParam('field')
440
441 # For image attachments
442 self.inline = bool(self.GetParam('inline'))
443 self.thumb = bool(self.GetParam('thumb'))
444
445 # For JS callbacks
446 self.token = self.GetParam('token')
447 self.starred = bool(self.GetIntParam('starred'))
448
449 # For issue reindexing utility servlet
450 self.auto_submit = self.GetParam('auto_submit')
451
452 # For issue dependency reranking servlet
453 self.parent_id = self.GetIntParam('parent_id')
454 self.target_id = self.GetIntParam('target_id')
455 self.moved_ids = self.GetIntListParam('moved_ids')
456 self.split_above = self.GetBoolParam('split_above')
457
458 # For adding issues to hotlists servlet
459 self.hotlist_ids_remove = self.GetIntListParam('hotlist_ids_remove')
460 self.hotlist_ids_add = self.GetIntListParam('hotlist_ids_add')
461 self.issue_refs = self.GetListParam('issue_refs')
462
463 def _ParseFormOverrides(self):
464 """Support deep linking by allowing the user to set form fields via QS."""
465 allowed_overrides = {
466 'template_name': self.GetParam('template_name'),
467 'initial_summary': self.GetParam('summary'),
468 'initial_description': (self.GetParam('description') or
469 self.GetParam('comment')),
470 'initial_comment': self.GetParam('comment'),
471 'initial_status': self.GetParam('status'),
472 'initial_owner': self.GetParam('owner'),
473 'initial_cc': self.GetParam('cc'),
474 'initial_blocked_on': self.GetParam('blockedon'),
475 'initial_blocking': self.GetParam('blocking'),
476 'initial_merge_into': self.GetIntParam('mergeinto'),
477 'initial_components': self.GetParam('components'),
478 'initial_hotlists': self.GetParam('hotlists'),
479
480 # For the people pages
481 'initial_add_members': self.GetParam('add_members'),
482 'initially_expanded_form': ezt.boolean(self.GetParam('expand_form')),
483
484 # For user group admin pages
485 'initial_name': (self.GetParam('group_name') or
486 self.GetParam('proposed_project_name')),
487 }
488
489 # Only keep the overrides that were actually provided in the query string.
490 self.form_overrides.update(
491 (k, v) for (k, v) in allowed_overrides.items()
492 if v is not None)
493
494 def _LookupViewedUser(self, services):
495 """Get information about the viewed user (if any) from the request."""
496 try:
497 with self.profiler.Phase('get viewed user, if any'):
498 self.viewed_user_auth = authdata.AuthData.FromEmail(
499 self.cnxn, self.viewed_username, services, autocreate=False)
500 except exceptions.NoSuchUserException:
501 logging.info('could not find user %r', self.viewed_username)
502 webapp2.abort(404, 'user not found')
503
504 if not self.viewed_user_auth.user_id:
505 webapp2.abort(404, 'user not found')
506
507 def _LookupProject(self, services):
508 """Get information about the current project (if any) from the request.
509
510 Raises:
511 NoSuchProjectException if there is no project with that name.
512 """
513 logging.info('project_name is %r', self.project_name)
514 if self.project_name:
515 self.project = services.project.GetProjectByName(
516 self.cnxn, self.project_name)
517 if not self.project:
518 raise exceptions.NoSuchProjectException()
519
520 def _LookupHotlist(self, services):
521 """Get information about the current hotlist (if any) from the request."""
522 with self.profiler.Phase('get current hotlist, if any'):
523 if self.hotlist_name:
524 hotlist_id_dict = services.features.LookupHotlistIDs(
525 self.cnxn, [self.hotlist_name], [self.viewed_user_auth.user_id])
526 try:
527 self.hotlist_id = hotlist_id_dict[(
528 self.hotlist_name, self.viewed_user_auth.user_id)]
529 except KeyError:
530 webapp2.abort(404, 'invalid hotlist')
531
532 if not self.hotlist_id:
533 logging.info('no hotlist_id or bad hotlist_name, so no hotlist')
534 else:
535 self.hotlist = services.features.GetHotlistByID(
536 self.cnxn, self.hotlist_id)
537 if not self.hotlist or (
538 self.viewed_user_auth.user_id and
539 self.viewed_user_auth.user_id not in self.hotlist.owner_ids):
540 webapp2.abort(404, 'invalid hotlist')
541
542 def _LookupLoggedInUser(self, services):
543 """Get information about the signed-in user (if any) from the request."""
544 self.auth = authdata.AuthData.FromRequest(self.cnxn, services)
545 self.me_user_id = (self.GetIntParam('me') or
546 self.viewed_user_auth.user_id or self.auth.user_id)
547
548 self.LookupLoggedInUserPerms(self.project)
549
550 def ComputeColSpec(self, config):
551 """Set col_spec based on param, default in the config, or site default."""
552 if self.col_spec is not None:
553 return # Already set.
554 default_col_spec = ''
555 if config:
556 default_col_spec = config.default_col_spec
557
558 col_spec = self.GetParam(
559 'colspec', default_value=default_col_spec,
560 antitamper_re=framework_constants.COLSPEC_RE)
561 cols_lower = col_spec.lower().split()
562 if self.project and any(
563 hotlist_col in cols_lower for hotlist_col in [
564 'rank', 'adder', 'added']):
565 # if the the list is a project list and the 'colspec' is a carry-over
566 # from hotlists, set col_spec to None so it will be set to default in
567 # in the next if statement
568 col_spec = None
569
570 if not col_spec:
571 # If col spec is still empty then default to the global col spec.
572 col_spec = tracker_constants.DEFAULT_COL_SPEC
573
574 self.col_spec = ' '.join(ParseColSpec(col_spec,
575 max_parts=framework_constants.MAX_COL_PARTS))
576
577 def PrepareForReentry(self, echo_data):
578 """Expose the results of form processing as if it was a new GET.
579
580 This method is called only when the user submits a form with invalid
581 information which they are being asked to correct it. Updating the MR
582 object allows the normal servlet get() method to populate the form with
583 the entered values and error messages.
584
585 Args:
586 echo_data: dict of {page_data_key: value_to_reoffer, ...} that will
587 override whatever HTML form values are nomally shown to the
588 user when they initially view the form. This allows them to
589 fix user input that was not valid.
590 """
591 self.form_overrides.update(echo_data)
592
593 def GetParam(self, query_param_name, default_value=None,
594 antitamper_re=None):
595 """Get a query parameter from the URL as a utf8 string."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200596 value = None
597 if hasattr(self.request, 'params'):
598 value = self.request.params.get(query_param_name)
599 else:
600 value = self.request.args.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000601 assert value is None or isinstance(value, six.text_type)
602 using_default = value is None
603 if using_default:
604 value = default_value
605
606 if antitamper_re and not antitamper_re.match(value):
607 if using_default:
608 logging.error('Default value fails antitamper for %s field: %s',
609 query_param_name, value)
610 else:
611 logging.info('User seems to have tampered with %s field: %s',
612 query_param_name, value)
613 raise exceptions.InputException()
614
615 return value
616
617 def GetIntParam(self, query_param_name, default_value=None):
618 """Get an integer param from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200619 value = None
620 if hasattr(self.request, 'params'):
621 value = self.request.params.get(query_param_name)
622 else:
623 value = self.request.args.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000624 if value is None or value == '':
625 return default_value
626
627 try:
628 return int(value)
629 except (TypeError, ValueError):
630 raise exceptions.InputException(
631 'Invalid value for integer param: %r' % value)
632
633 def GetPositiveIntParam(self, query_param_name, default_value=None):
634 """Returns 0 if the user-provided value is less than 0."""
635 return max(self.GetIntParam(query_param_name, default_value=default_value),
636 0)
637
638 def GetListParam(self, query_param_name, default_value=None):
639 """Get a list of strings from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200640 params = None
641 if hasattr(self.request, 'params'):
642 params = self.request.params.get(query_param_name)
643 else:
644 params = self.request.args.get(query_param_name)
Copybara854996b2021-09-07 19:36:02 +0000645 if params is None:
646 return default_value
647 if not params:
648 return []
649 return params.split(',')
650
651 def GetIntListParam(self, query_param_name, default_value=None):
652 """Get a list of ints from the URL or default."""
653 param_list = self.GetListParam(query_param_name)
654 if param_list is None:
655 return default_value
656
657 try:
658 return [int(p) for p in param_list]
659 except (TypeError, ValueError):
660 raise exceptions.InputException('Invalid value for integer list param')
661
662 def GetBoolParam(self, query_param_name, default_value=None):
663 """Get a boolean param from the URL or default."""
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200664 value = None
665 if hasattr(self.request, 'params'):
666 value = self.request.params.get(query_param_name)
667 else:
668 value = self.request.args.get(query_param_name)
669
Copybara854996b2021-09-07 19:36:02 +0000670 if value is None:
671 return default_value
672
673 if (not value) or (value.lower() == 'false'):
674 return False
675 return True
676
677
678def _ParsePathIdentifiers(path):
679 """Parse out the workspace being requested (if any).
680
681 Args:
682 path: A string beginning with the request's path info.
683
684 Returns:
685 (viewed_user_val, project_name).
686 """
687 viewed_user_val = None
688 project_name = None
689 hotlist_id = None
690 hotlist_name = None
691
692 # Strip off any query params
693 split_path = path.lstrip('/').split('?')[0].split('/')
694 if len(split_path) >= 2:
695 if split_path[0] == 'hotlists':
696 if split_path[1].isdigit():
697 hotlist_id = int(split_path[1])
698 if split_path[0] == 'p':
699 project_name = split_path[1]
700 if split_path[0] == 'u' or split_path[0] == 'users':
701 viewed_user_val = urllib.unquote(split_path[1])
702 if len(split_path) >= 4 and split_path[2] == 'hotlists':
703 try:
704 hotlist_id = int(
705 urllib.unquote(split_path[3].split('.')[0]))
706 except ValueError:
707 raw_last_path = (split_path[3][:-3] if
708 split_path[3].endswith('.do') else split_path[3])
709 last_path = urllib.unquote(raw_last_path)
710 match = framework_bizobj.RE_HOTLIST_NAME.match(
711 last_path)
712 if not match:
713 raise exceptions.InputException(
714 'Could not parse hotlist id or name')
715 else:
716 hotlist_name = last_path.lower()
717
718 if split_path[0] == 'g':
719 viewed_user_val = urllib.unquote(split_path[1])
720
721 return viewed_user_val, project_name, hotlist_id, hotlist_name
722
723
724def _GetViewedEmail(viewed_user_val, cnxn, services):
725 """Returns the viewed user's email.
726
727 Args:
728 viewed_user_val: Could be either int (user_id) or str (email).
729 cnxn: connection to the SQL database.
730 services: Interface to all persistence storage backends.
731
732 Returns:
733 viewed_email
734 """
735 if not viewed_user_val:
736 return None
737
738 try:
739 viewed_userid = int(viewed_user_val)
740 viewed_email = services.user.LookupUserEmail(cnxn, viewed_userid)
741 if not viewed_email:
742 logging.info('userID %s not found', viewed_userid)
743 webapp2.abort(404, 'user not found')
744 except ValueError:
745 viewed_email = viewed_user_val
746
747 return viewed_email
748
749
750def ParseColSpec(
751 col_spec, max_parts=framework_constants.MAX_SORT_PARTS,
752 ignore=None):
753 """Split a string column spec into a list of column names.
754
755 We dedup col parts because an attacker could try to DoS us or guess
756 zero or one result by measuring the time to process a request that
757 has a very long column list.
758
759 Args:
760 col_spec: a unicode string containing a list of labels.
761 max_parts: optional int maximum number of parts to consider.
762 ignore: optional list of column name parts to ignore.
763
764 Returns:
765 A list of the extracted labels. Non-alphanumeric
766 characters other than the period will be stripped from the text.
767 """
768 cols = framework_constants.COLSPEC_COL_RE.findall(col_spec)
769 result = [] # List of column headers with no duplicates.
770 # Set of column parts that we have processed so far.
771 seen = set()
772 if ignore:
773 seen = set(ignore_col.lower() for ignore_col in ignore)
774 max_parts += len(ignore)
775
776 for col in cols:
777 parts = []
778 for part in col.split('/'):
779 if (part.lower() not in seen and len(seen) < max_parts
780 and len(part) < framework_constants.MAX_COL_LEN):
781 parts.append(part)
782 seen.add(part.lower())
783 if parts:
784 result.append('/'.join(parts))
785 return result