Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/framework/monorailrequest.py b/framework/monorailrequest.py
new file mode 100644
index 0000000..e51aa15
--- /dev/null
+++ b/framework/monorailrequest.py
@@ -0,0 +1,713 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes to hold information parsed from a request.
+
+To simplify our servlets and avoid duplication of code, we parse some
+info out of the request as soon as we get it and then pass a MonorailRequest
+object to the servlet-specific request handler methods.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import endpoints
+import logging
+import re
+import urllib
+
+import ezt
+import six
+
+from google.appengine.api import app_identity
+from google.appengine.api import oauth
+
+import webapp2
+
+import settings
+from businesslogic import work_env
+from features import features_constants
+from framework import authdata
+from framework import exceptions
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_views
+from framework import monorailcontext
+from framework import permissions
+from framework import profiler
+from framework import sql
+from framework import template_helpers
+from proto import api_pb2_v1
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+
+
+_HOSTPORT_RE = re.compile('^[-a-z0-9.]+(:\d+)?$', re.I)
+
+
+# TODO(jrobbins): Stop extending MonorailContext and change whole servlet
+# framework to pass around separate objects for mc and mr.
+class MonorailRequestBase(monorailcontext.MonorailContext):
+ """A base class with common attributes for internal and external requests."""
+
+ def __init__(self, services, requester=None, cnxn=None):
+ super(MonorailRequestBase, self).__init__(
+ services, cnxn=cnxn, requester=requester)
+
+ self.project_name = None
+ self.project = None
+ self.config = None
+
+ @property
+ def project_id(self):
+ return self.project.project_id if self.project else None
+
+
+class MonorailApiRequest(MonorailRequestBase):
+ """A class to hold information parsed from the Endpoints API request."""
+
+ # pylint: disable=attribute-defined-outside-init
+ def __init__(self, request, services, cnxn=None):
+ requester_object = (
+ endpoints.get_current_user() or
+ oauth.get_current_user(
+ framework_constants.OAUTH_SCOPE))
+ requester = requester_object.email().lower()
+ super(MonorailApiRequest, self).__init__(
+ services, requester=requester, cnxn=cnxn)
+ self.me_user_id = self.auth.user_id
+ self.viewed_username = None
+ self.viewed_user_auth = None
+ self.issue = None
+ self.granted_perms = set()
+
+ # query parameters
+ self.params = {
+ 'can': 1,
+ 'start': 0,
+ 'num': tracker_constants.DEFAULT_RESULTS_PER_PAGE,
+ 'q': '',
+ 'sort': '',
+ 'groupby': '',
+ 'projects': [],
+ 'hotlists': []
+ }
+ self.use_cached_searches = True
+ self.mode = None
+
+ if hasattr(request, 'projectId'):
+ self.project_name = request.projectId
+ with work_env.WorkEnv(self, services) as we:
+ self.project = we.GetProjectByName(self.project_name)
+ self.params['projects'].append(self.project_name)
+ self.config = we.GetProjectConfig(self.project_id)
+ if hasattr(request, 'additionalProject'):
+ self.params['projects'].extend(request.additionalProject)
+ self.params['projects'] = list(set(self.params['projects']))
+ self.LookupLoggedInUserPerms(self.project)
+ if hasattr(request, 'projectId'):
+ with work_env.WorkEnv(self, services) as we:
+ if hasattr(request, 'issueId'):
+ self.issue = we.GetIssueByLocalID(
+ self.project_id, request.issueId, use_cache=False)
+ self.granted_perms = tracker_bizobj.GetGrantedPerms(
+ self.issue, self.auth.effective_ids, self.config)
+ if hasattr(request, 'userId'):
+ self.viewed_username = request.userId.lower()
+ if self.viewed_username == 'me':
+ self.viewed_username = requester
+ self.viewed_user_auth = authdata.AuthData.FromEmail(
+ self.cnxn, self.viewed_username, services)
+ elif hasattr(request, 'groupName'):
+ self.viewed_username = request.groupName.lower()
+ try:
+ self.viewed_user_auth = authdata.AuthData.FromEmail(
+ self.cnxn, self.viewed_username, services)
+ except exceptions.NoSuchUserException:
+ self.viewed_user_auth = None
+
+ # Build q.
+ if hasattr(request, 'q') and request.q:
+ self.params['q'] = request.q
+ if hasattr(request, 'publishedMax') and request.publishedMax:
+ self.params['q'] += ' opened<=%d' % request.publishedMax
+ if hasattr(request, 'publishedMin') and request.publishedMin:
+ self.params['q'] += ' opened>=%d' % request.publishedMin
+ if hasattr(request, 'updatedMax') and request.updatedMax:
+ self.params['q'] += ' modified<=%d' % request.updatedMax
+ if hasattr(request, 'updatedMin') and request.updatedMin:
+ self.params['q'] += ' modified>=%d' % request.updatedMin
+ if hasattr(request, 'owner') and request.owner:
+ self.params['q'] += ' owner:%s' % request.owner
+ if hasattr(request, 'status') and request.status:
+ self.params['q'] += ' status:%s' % request.status
+ if hasattr(request, 'label') and request.label:
+ self.params['q'] += ' label:%s' % request.label
+
+ if hasattr(request, 'can') and request.can:
+ if request.can == api_pb2_v1.CannedQuery.all:
+ self.params['can'] = 1
+ elif request.can == api_pb2_v1.CannedQuery.new:
+ self.params['can'] = 6
+ elif request.can == api_pb2_v1.CannedQuery.open:
+ self.params['can'] = 2
+ elif request.can == api_pb2_v1.CannedQuery.owned:
+ self.params['can'] = 3
+ elif request.can == api_pb2_v1.CannedQuery.reported:
+ self.params['can'] = 4
+ elif request.can == api_pb2_v1.CannedQuery.starred:
+ self.params['can'] = 5
+ elif request.can == api_pb2_v1.CannedQuery.to_verify:
+ self.params['can'] = 7
+ else: # Endpoints should have caught this.
+ raise exceptions.InputException(
+ 'Canned query %s is not supported.', request.can)
+ if hasattr(request, 'startIndex') and request.startIndex:
+ self.params['start'] = request.startIndex
+ if hasattr(request, 'maxResults') and request.maxResults:
+ self.params['num'] = request.maxResults
+ if hasattr(request, 'sort') and request.sort:
+ self.params['sort'] = request.sort
+
+ self.query_project_names = self.GetParam('projects')
+ self.group_by_spec = self.GetParam('groupby')
+ self.group_by_spec = ' '.join(ParseColSpec(
+ self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
+ self.sort_spec = self.GetParam('sort')
+ self.sort_spec = ' '.join(ParseColSpec(self.sort_spec))
+ self.query = self.GetParam('q')
+ self.can = self.GetParam('can')
+ self.start = self.GetParam('start')
+ self.num = self.GetParam('num')
+
+ def GetParam(self, query_param_name, default_value=None,
+ _antitamper_re=None):
+ return self.params.get(query_param_name, default_value)
+
+ def GetPositiveIntParam(self, query_param_name, default_value=None):
+ """Returns 0 if the user-provided value is less than 0."""
+ return max(self.GetParam(query_param_name, default_value=default_value),
+ 0)
+
+
+class MonorailRequest(MonorailRequestBase):
+ """A class to hold information parsed from the HTTP request.
+
+ The goal of MonorailRequest is to do almost all URL path and query string
+ procesing in one place, which makes the servlet code simpler.
+
+ Attributes:
+ cnxn: connection to the SQL databases.
+ logged_in_user_id: int user ID of the signed-in user, or None.
+ effective_ids: set of signed-in user ID and all their user group IDs.
+ user_pb: User object for the signed in user.
+ project_name: string name of the current project.
+ project_id: int ID of the current projet.
+ viewed_username: string username of the user whose profile is being viewed.
+ can: int "canned query" number to scope the user's search.
+ num: int number of results to show per pagination page.
+ start: int position in result set to show on this pagination page.
+ etc: there are many more, all read-only.
+ """
+
+ # pylint: disable=attribute-defined-outside-init
+ def __init__(self, services, params=None):
+ """Initialize the MonorailRequest object."""
+ # Note: mr starts off assuming anon until ParseRequest() is called.
+ super(MonorailRequest, self).__init__(services)
+ self.form_overrides = {}
+ if params:
+ self.form_overrides.update(params)
+ self.debug_enabled = False
+ self.use_cached_searches = True
+
+ self.hotlist_id = None
+ self.hotlist = None
+ self.hotlist_name = None
+
+ self.viewed_username = None
+ self.viewed_user_auth = authdata.AuthData()
+
+ def ParseRequest(self, request, services, do_user_lookups=True):
+ """Parse tons of useful info from the given request object.
+
+ Args:
+ request: webapp2 Request object w/ path and query params.
+ services: connections to backend servers including DB.
+ do_user_lookups: Set to False to disable lookups during testing.
+ """
+ with self.profiler.Phase('basic parsing'):
+ self.request = request
+ self.current_page_url = request.url
+ self.current_page_url_encoded = urllib.quote_plus(self.current_page_url)
+
+ # Only accept a hostport from the request that looks valid.
+ if not _HOSTPORT_RE.match(request.host):
+ raise exceptions.InputException(
+ 'request.host looks funny: %r', request.host)
+
+ logging.info('Request: %s', self.current_page_url)
+
+ with self.profiler.Phase('path parsing'):
+ (viewed_user_val, self.project_name,
+ self.hotlist_id, self.hotlist_name) = _ParsePathIdentifiers(
+ self.request.path)
+ self.viewed_username = _GetViewedEmail(
+ viewed_user_val, self.cnxn, services)
+ with self.profiler.Phase('qs parsing'):
+ self._ParseQueryParameters()
+ with self.profiler.Phase('overrides parsing'):
+ self._ParseFormOverrides()
+
+ if not self.project: # It can be already set in unit tests.
+ self._LookupProject(services)
+ if self.project_id and services.config:
+ self.config = services.config.GetProjectConfig(self.cnxn, self.project_id)
+
+ if do_user_lookups:
+ if self.viewed_username:
+ self._LookupViewedUser(services)
+ self._LookupLoggedInUser(services)
+ # TODO(jrobbins): re-implement HandleLurkerViewingSelf()
+
+ if not self.hotlist:
+ self._LookupHotlist(services)
+
+ if self.query is None:
+ self.query = self._CalcDefaultQuery()
+
+ prod_debug_allowed = self.perms.HasPerm(
+ permissions.VIEW_DEBUG, self.auth.user_id, None)
+ self.debug_enabled = (request.params.get('debug') and
+ (settings.local_mode or prod_debug_allowed))
+ # temporary option for perf testing on staging instance.
+ if request.params.get('disable_cache'):
+ if settings.local_mode or 'staging' in request.host:
+ self.use_cached_searches = False
+
+ def _CalcDefaultQuery(self):
+ """When URL has no q= param, return the default for members or ''."""
+ if (self.can == 2 and self.project and self.auth.effective_ids and
+ framework_bizobj.UserIsInProject(self.project, self.auth.effective_ids)
+ and self.config):
+ return self.config.member_default_query
+ else:
+ return ''
+
+ def _ParseQueryParameters(self):
+ """Parse and convert all the query string params used in any servlet."""
+ self.start = self.GetPositiveIntParam('start', default_value=0)
+ self.num = self.GetPositiveIntParam(
+ 'num', default_value=tracker_constants.DEFAULT_RESULTS_PER_PAGE)
+ # Prevent DoS attacks that try to make us serve really huge result pages.
+ self.num = min(self.num, settings.max_artifact_search_results_per_page)
+
+ self.invalidation_timestep = self.GetIntParam(
+ 'invalidation_timestep', default_value=0)
+
+ self.continue_issue_id = self.GetIntParam(
+ 'continue_issue_id', default_value=0)
+ self.redir = self.GetParam('redir')
+
+ # Search scope, a.k.a., canned query ID
+ # TODO(jrobbins): make configurable
+ self.can = self.GetIntParam(
+ 'can', default_value=tracker_constants.OPEN_ISSUES_CAN)
+
+ # Search query
+ self.query = self.GetParam('q')
+
+ # Sorting of search results (needed for result list and flipper)
+ self.sort_spec = self.GetParam(
+ 'sort', default_value='',
+ antitamper_re=framework_constants.SORTSPEC_RE)
+ self.sort_spec = ' '.join(ParseColSpec(self.sort_spec))
+
+ # Note: This is set later in request handling by ComputeColSpec().
+ self.col_spec = None
+
+ # Grouping of search results (needed for result list and flipper)
+ self.group_by_spec = self.GetParam(
+ 'groupby', default_value='',
+ antitamper_re=framework_constants.SORTSPEC_RE)
+ self.group_by_spec = ' '.join(ParseColSpec(
+ self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
+
+ # For issue list and grid mode.
+ self.cursor = self.GetParam('cursor')
+ self.preview = self.GetParam('preview')
+ self.mode = self.GetParam('mode') or 'list'
+ self.x = self.GetParam('x', default_value='')
+ self.y = self.GetParam('y', default_value='')
+ self.cells = self.GetParam('cells', default_value='ids')
+
+ # For the dashboard and issue lists included in the dashboard.
+ self.ajah = self.GetParam('ajah') # AJAH = Asychronous Javascript And HTML
+ self.table_title = self.GetParam('table_title')
+ self.panel_id = self.GetIntParam('panel')
+
+ # For pagination of updates lists
+ self.before = self.GetPositiveIntParam('before')
+ self.after = self.GetPositiveIntParam('after')
+
+ # For cron tasks and backend calls
+ self.lower_bound = self.GetIntParam('lower_bound')
+ self.upper_bound = self.GetIntParam('upper_bound')
+ self.shard_id = self.GetIntParam('shard_id')
+
+ # For specifying which objects to operate on
+ self.local_id = self.GetIntParam('id')
+ self.local_id_list = self.GetIntListParam('ids')
+ self.seq = self.GetIntParam('seq')
+ self.aid = self.GetIntParam('aid')
+ self.signed_aid = self.GetParam('signed_aid')
+ self.specified_user_id = self.GetIntParam('u', default_value=0)
+ self.specified_logged_in_user_id = self.GetIntParam(
+ 'logged_in_user_id', default_value=0)
+ self.specified_me_user_ids = self.GetIntListParam('me_user_ids')
+
+ # TODO(jrobbins): Phase this out after next deployment. If an old
+ # version of the default GAE module sends a request with the old
+ # me_user_id= parameter, then accept it.
+ specified_me_user_id = self.GetIntParam(
+ 'me_user_id', default_value=0)
+ if specified_me_user_id:
+ self.specified_me_user_ids = [specified_me_user_id]
+
+ self.specified_project = self.GetParam('project')
+ self.specified_project_id = self.GetIntParam('project_id')
+ self.query_project_names = self.GetListParam('projects', default_value=[])
+ self.template_name = self.GetParam('template')
+ self.component_path = self.GetParam('component')
+ self.field_name = self.GetParam('field')
+
+ # For image attachments
+ self.inline = bool(self.GetParam('inline'))
+ self.thumb = bool(self.GetParam('thumb'))
+
+ # For JS callbacks
+ self.token = self.GetParam('token')
+ self.starred = bool(self.GetIntParam('starred'))
+
+ # For issue reindexing utility servlet
+ self.auto_submit = self.GetParam('auto_submit')
+
+ # For issue dependency reranking servlet
+ self.parent_id = self.GetIntParam('parent_id')
+ self.target_id = self.GetIntParam('target_id')
+ self.moved_ids = self.GetIntListParam('moved_ids')
+ self.split_above = self.GetBoolParam('split_above')
+
+ # For adding issues to hotlists servlet
+ self.hotlist_ids_remove = self.GetIntListParam('hotlist_ids_remove')
+ self.hotlist_ids_add = self.GetIntListParam('hotlist_ids_add')
+ self.issue_refs = self.GetListParam('issue_refs')
+
+ def _ParseFormOverrides(self):
+ """Support deep linking by allowing the user to set form fields via QS."""
+ allowed_overrides = {
+ 'template_name': self.GetParam('template_name'),
+ 'initial_summary': self.GetParam('summary'),
+ 'initial_description': (self.GetParam('description') or
+ self.GetParam('comment')),
+ 'initial_comment': self.GetParam('comment'),
+ 'initial_status': self.GetParam('status'),
+ 'initial_owner': self.GetParam('owner'),
+ 'initial_cc': self.GetParam('cc'),
+ 'initial_blocked_on': self.GetParam('blockedon'),
+ 'initial_blocking': self.GetParam('blocking'),
+ 'initial_merge_into': self.GetIntParam('mergeinto'),
+ 'initial_components': self.GetParam('components'),
+ 'initial_hotlists': self.GetParam('hotlists'),
+
+ # For the people pages
+ 'initial_add_members': self.GetParam('add_members'),
+ 'initially_expanded_form': ezt.boolean(self.GetParam('expand_form')),
+
+ # For user group admin pages
+ 'initial_name': (self.GetParam('group_name') or
+ self.GetParam('proposed_project_name')),
+ }
+
+ # Only keep the overrides that were actually provided in the query string.
+ self.form_overrides.update(
+ (k, v) for (k, v) in allowed_overrides.items()
+ if v is not None)
+
+ def _LookupViewedUser(self, services):
+ """Get information about the viewed user (if any) from the request."""
+ try:
+ with self.profiler.Phase('get viewed user, if any'):
+ self.viewed_user_auth = authdata.AuthData.FromEmail(
+ self.cnxn, self.viewed_username, services, autocreate=False)
+ except exceptions.NoSuchUserException:
+ logging.info('could not find user %r', self.viewed_username)
+ webapp2.abort(404, 'user not found')
+
+ if not self.viewed_user_auth.user_id:
+ webapp2.abort(404, 'user not found')
+
+ def _LookupProject(self, services):
+ """Get information about the current project (if any) from the request.
+
+ Raises:
+ NoSuchProjectException if there is no project with that name.
+ """
+ logging.info('project_name is %r', self.project_name)
+ if self.project_name:
+ self.project = services.project.GetProjectByName(
+ self.cnxn, self.project_name)
+ if not self.project:
+ raise exceptions.NoSuchProjectException()
+
+ def _LookupHotlist(self, services):
+ """Get information about the current hotlist (if any) from the request."""
+ with self.profiler.Phase('get current hotlist, if any'):
+ if self.hotlist_name:
+ hotlist_id_dict = services.features.LookupHotlistIDs(
+ self.cnxn, [self.hotlist_name], [self.viewed_user_auth.user_id])
+ try:
+ self.hotlist_id = hotlist_id_dict[(
+ self.hotlist_name, self.viewed_user_auth.user_id)]
+ except KeyError:
+ webapp2.abort(404, 'invalid hotlist')
+
+ if not self.hotlist_id:
+ logging.info('no hotlist_id or bad hotlist_name, so no hotlist')
+ else:
+ self.hotlist = services.features.GetHotlistByID(
+ self.cnxn, self.hotlist_id)
+ if not self.hotlist or (
+ self.viewed_user_auth.user_id and
+ self.viewed_user_auth.user_id not in self.hotlist.owner_ids):
+ webapp2.abort(404, 'invalid hotlist')
+
+ def _LookupLoggedInUser(self, services):
+ """Get information about the signed-in user (if any) from the request."""
+ self.auth = authdata.AuthData.FromRequest(self.cnxn, services)
+ self.me_user_id = (self.GetIntParam('me') or
+ self.viewed_user_auth.user_id or self.auth.user_id)
+
+ self.LookupLoggedInUserPerms(self.project)
+
+ def ComputeColSpec(self, config):
+ """Set col_spec based on param, default in the config, or site default."""
+ if self.col_spec is not None:
+ return # Already set.
+ default_col_spec = ''
+ if config:
+ default_col_spec = config.default_col_spec
+
+ col_spec = self.GetParam(
+ 'colspec', default_value=default_col_spec,
+ antitamper_re=framework_constants.COLSPEC_RE)
+ cols_lower = col_spec.lower().split()
+ if self.project and any(
+ hotlist_col in cols_lower for hotlist_col in [
+ 'rank', 'adder', 'added']):
+ # if the the list is a project list and the 'colspec' is a carry-over
+ # from hotlists, set col_spec to None so it will be set to default in
+ # in the next if statement
+ col_spec = None
+
+ if not col_spec:
+ # If col spec is still empty then default to the global col spec.
+ col_spec = tracker_constants.DEFAULT_COL_SPEC
+
+ self.col_spec = ' '.join(ParseColSpec(col_spec,
+ max_parts=framework_constants.MAX_COL_PARTS))
+
+ def PrepareForReentry(self, echo_data):
+ """Expose the results of form processing as if it was a new GET.
+
+ This method is called only when the user submits a form with invalid
+ information which they are being asked to correct it. Updating the MR
+ object allows the normal servlet get() method to populate the form with
+ the entered values and error messages.
+
+ Args:
+ echo_data: dict of {page_data_key: value_to_reoffer, ...} that will
+ override whatever HTML form values are nomally shown to the
+ user when they initially view the form. This allows them to
+ fix user input that was not valid.
+ """
+ self.form_overrides.update(echo_data)
+
+ def GetParam(self, query_param_name, default_value=None,
+ antitamper_re=None):
+ """Get a query parameter from the URL as a utf8 string."""
+ value = self.request.params.get(query_param_name)
+ assert value is None or isinstance(value, six.text_type)
+ using_default = value is None
+ if using_default:
+ value = default_value
+
+ if antitamper_re and not antitamper_re.match(value):
+ if using_default:
+ logging.error('Default value fails antitamper for %s field: %s',
+ query_param_name, value)
+ else:
+ logging.info('User seems to have tampered with %s field: %s',
+ query_param_name, value)
+ raise exceptions.InputException()
+
+ return value
+
+ def GetIntParam(self, query_param_name, default_value=None):
+ """Get an integer param from the URL or default."""
+ value = self.request.params.get(query_param_name)
+ if value is None or value == '':
+ return default_value
+
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ raise exceptions.InputException(
+ 'Invalid value for integer param: %r' % value)
+
+ def GetPositiveIntParam(self, query_param_name, default_value=None):
+ """Returns 0 if the user-provided value is less than 0."""
+ return max(self.GetIntParam(query_param_name, default_value=default_value),
+ 0)
+
+ def GetListParam(self, query_param_name, default_value=None):
+ """Get a list of strings from the URL or default."""
+ params = self.request.params.get(query_param_name)
+ if params is None:
+ return default_value
+ if not params:
+ return []
+ return params.split(',')
+
+ def GetIntListParam(self, query_param_name, default_value=None):
+ """Get a list of ints from the URL or default."""
+ param_list = self.GetListParam(query_param_name)
+ if param_list is None:
+ return default_value
+
+ try:
+ return [int(p) for p in param_list]
+ except (TypeError, ValueError):
+ raise exceptions.InputException('Invalid value for integer list param')
+
+ def GetBoolParam(self, query_param_name, default_value=None):
+ """Get a boolean param from the URL or default."""
+ value = self.request.params.get(query_param_name)
+ if value is None:
+ return default_value
+
+ if (not value) or (value.lower() == 'false'):
+ return False
+ return True
+
+
+def _ParsePathIdentifiers(path):
+ """Parse out the workspace being requested (if any).
+
+ Args:
+ path: A string beginning with the request's path info.
+
+ Returns:
+ (viewed_user_val, project_name).
+ """
+ viewed_user_val = None
+ project_name = None
+ hotlist_id = None
+ hotlist_name = None
+
+ # Strip off any query params
+ split_path = path.lstrip('/').split('?')[0].split('/')
+ if len(split_path) >= 2:
+ if split_path[0] == 'hotlists':
+ if split_path[1].isdigit():
+ hotlist_id = int(split_path[1])
+ if split_path[0] == 'p':
+ project_name = split_path[1]
+ if split_path[0] == 'u' or split_path[0] == 'users':
+ viewed_user_val = urllib.unquote(split_path[1])
+ if len(split_path) >= 4 and split_path[2] == 'hotlists':
+ try:
+ hotlist_id = int(
+ urllib.unquote(split_path[3].split('.')[0]))
+ except ValueError:
+ raw_last_path = (split_path[3][:-3] if
+ split_path[3].endswith('.do') else split_path[3])
+ last_path = urllib.unquote(raw_last_path)
+ match = framework_bizobj.RE_HOTLIST_NAME.match(
+ last_path)
+ if not match:
+ raise exceptions.InputException(
+ 'Could not parse hotlist id or name')
+ else:
+ hotlist_name = last_path.lower()
+
+ if split_path[0] == 'g':
+ viewed_user_val = urllib.unquote(split_path[1])
+
+ return viewed_user_val, project_name, hotlist_id, hotlist_name
+
+
+def _GetViewedEmail(viewed_user_val, cnxn, services):
+ """Returns the viewed user's email.
+
+ Args:
+ viewed_user_val: Could be either int (user_id) or str (email).
+ cnxn: connection to the SQL database.
+ services: Interface to all persistence storage backends.
+
+ Returns:
+ viewed_email
+ """
+ if not viewed_user_val:
+ return None
+
+ try:
+ viewed_userid = int(viewed_user_val)
+ viewed_email = services.user.LookupUserEmail(cnxn, viewed_userid)
+ if not viewed_email:
+ logging.info('userID %s not found', viewed_userid)
+ webapp2.abort(404, 'user not found')
+ except ValueError:
+ viewed_email = viewed_user_val
+
+ return viewed_email
+
+
+def ParseColSpec(
+ col_spec, max_parts=framework_constants.MAX_SORT_PARTS,
+ ignore=None):
+ """Split a string column spec into a list of column names.
+
+ We dedup col parts because an attacker could try to DoS us or guess
+ zero or one result by measuring the time to process a request that
+ has a very long column list.
+
+ Args:
+ col_spec: a unicode string containing a list of labels.
+ max_parts: optional int maximum number of parts to consider.
+ ignore: optional list of column name parts to ignore.
+
+ Returns:
+ A list of the extracted labels. Non-alphanumeric
+ characters other than the period will be stripped from the text.
+ """
+ cols = framework_constants.COLSPEC_COL_RE.findall(col_spec)
+ result = [] # List of column headers with no duplicates.
+ # Set of column parts that we have processed so far.
+ seen = set()
+ if ignore:
+ seen = set(ignore_col.lower() for ignore_col in ignore)
+ max_parts += len(ignore)
+
+ for col in cols:
+ parts = []
+ for part in col.split('/'):
+ if (part.lower() not in seen and len(seen) < max_parts
+ and len(part) < framework_constants.MAX_COL_LEN):
+ parts.append(part)
+ seen.add(part.lower())
+ if parts:
+ result.append('/'.join(parts))
+ return result