OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered 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 |
| 8 To simplify our servlets and avoid duplication of code, we parse some |
| 9 info out of the request as soon as we get it and then pass a MonorailRequest |
| 10 object to the servlet-specific request handler methods. |
| 11 """ |
| 12 |
| 13 import endpoints |
| 14 import logging |
| 15 import re |
| 16 import urllib |
| 17 |
| 18 from third_party import ezt |
| 19 |
| 20 from google.appengine.api import app_identity |
| 21 from google.appengine.api import oauth |
| 22 from google.appengine.api import users |
| 23 |
| 24 import webapp2 |
| 25 |
| 26 import settings |
| 27 from framework import framework_constants |
| 28 from framework import framework_views |
| 29 from framework import permissions |
| 30 from framework import sql |
| 31 from framework import template_helpers |
| 32 from proto import api_pb2_v1 |
| 33 from proto import user_pb2 |
| 34 from services import user_svc |
| 35 from tracker import tracker_bizobj |
| 36 from tracker import tracker_constants |
| 37 |
| 38 |
| 39 _HOSTPORT_RE = re.compile('^[-a-z0-9.]+(:\d+)?$', re.I) |
| 40 |
| 41 |
| 42 class AuthData(object): |
| 43 """This object holds authentication data about a user. |
| 44 |
| 45 This is used by MonorailRequest as it determines which user the |
| 46 requester is authenticated as and fetches the user's data. It can |
| 47 also be used to lookup perms for user IDs specified in issue fields. |
| 48 |
| 49 Attributes: |
| 50 user_id: The user ID of the user (or 0 if not signed in). |
| 51 effective_ids: A set of user IDs that includes the signed in user's |
| 52 direct user ID and the user IDs of all their user groups. |
| 53 This set will be empty for anonymous users. |
| 54 user_view: UserView object for the signed-in user. |
| 55 user_pb: User object for the signed-in user. |
| 56 email: email address for the user, or None. |
| 57 """ |
| 58 |
| 59 def __init__(self): |
| 60 self.user_id = 0 |
| 61 self.effective_ids = set() |
| 62 self.user_view = None |
| 63 self.user_pb = user_pb2.MakeUser() |
| 64 self.email = None |
| 65 |
| 66 @classmethod |
| 67 def FromRequest(cls, cnxn, services): |
| 68 """Determine auth information from the request and fetches user data. |
| 69 |
| 70 If everything works and the user is signed in, then all of the public |
| 71 attributes of the AuthData instance will be filled in appropriately. |
| 72 |
| 73 Args: |
| 74 cnxn: connection to the SQL database. |
| 75 services: Interface to all persistence storage backends. |
| 76 |
| 77 Returns: |
| 78 A new AuthData object. |
| 79 """ |
| 80 user = users.get_current_user() |
| 81 if user is None: |
| 82 return cls() |
| 83 else: |
| 84 # We create a User row for each user who visits the site. |
| 85 # TODO(jrobbins): we should really only do it when they take action. |
| 86 return cls.FromEmail(cnxn, user.email(), services, autocreate=True) |
| 87 |
| 88 @classmethod |
| 89 def FromEmail(cls, cnxn, email, services, autocreate=False): |
| 90 """Determine auth information for the given user email address. |
| 91 |
| 92 Args: |
| 93 cnxn: monorail connection to the database. |
| 94 email: string email address of the user. |
| 95 services: connections to backend servers. |
| 96 autocreate: set to True to create a new row in the Users table if needed. |
| 97 |
| 98 Returns: |
| 99 A new AuthData object. |
| 100 |
| 101 Raises: |
| 102 user_svc.NoSuchUserException: If the user of the email does not exist. |
| 103 """ |
| 104 auth = cls() |
| 105 auth.email = email |
| 106 if email: |
| 107 auth.user_id = services.user.LookupUserID( |
| 108 cnxn, email, autocreate=autocreate) |
| 109 assert auth.user_id |
| 110 |
| 111 cls._FinishInitialization(cnxn, auth, services) |
| 112 return auth |
| 113 |
| 114 @classmethod |
| 115 def FromUserID(cls, cnxn, user_id, services): |
| 116 """Determine auth information for the given user ID. |
| 117 |
| 118 Args: |
| 119 cnxn: monorail connection to the database. |
| 120 user_id: int user ID of the user. |
| 121 services: connections to backend servers. |
| 122 |
| 123 Returns: |
| 124 A new AuthData object. |
| 125 """ |
| 126 auth = cls() |
| 127 auth.user_id = user_id |
| 128 if auth.user_id: |
| 129 auth.email = services.user.LookupUserEmail(cnxn, user_id) |
| 130 |
| 131 cls._FinishInitialization(cnxn, auth, services) |
| 132 return auth |
| 133 |
| 134 @classmethod |
| 135 def _FinishInitialization(cls, cnxn, auth, services): |
| 136 """Fill in the test of the fields based on the user_id.""" |
| 137 # TODO(jrobbins): re-implement same_org |
| 138 if auth.user_id: |
| 139 auth.effective_ids = services.usergroup.LookupMemberships( |
| 140 cnxn, auth.user_id) |
| 141 auth.effective_ids.add(auth.user_id) |
| 142 auth.user_pb = services.user.GetUser(cnxn, auth.user_id) |
| 143 if auth.user_pb: |
| 144 auth.user_view = framework_views.UserView( |
| 145 auth.user_id, auth.email, |
| 146 auth.user_pb.obscure_email) |
| 147 |
| 148 |
| 149 class MonorailApiRequest(object): |
| 150 """A class to hold information parsed from the Endpoints API request.""" |
| 151 |
| 152 # pylint: disable=attribute-defined-outside-init |
| 153 def __init__(self, request, services): |
| 154 requester = ( |
| 155 endpoints.get_current_user() or |
| 156 oauth.get_current_user( |
| 157 framework_constants.OAUTH_SCOPE)) |
| 158 requester_email = requester.email().lower() |
| 159 self.cnxn = sql.MonorailConnection() |
| 160 self.auth = AuthData.FromEmail( |
| 161 self.cnxn, requester_email, services) |
| 162 self.me_user_id = self.auth.user_id |
| 163 self.viewed_username = None |
| 164 self.viewed_user_auth = None |
| 165 self.project_name = None |
| 166 self.project = None |
| 167 self.issue = None |
| 168 self.config = None |
| 169 self.granted_perms = set() |
| 170 |
| 171 # query parameters |
| 172 self.params = { |
| 173 'can': 1, |
| 174 'start': 0, |
| 175 'num': 100, |
| 176 'q': '', |
| 177 'sort': '', |
| 178 'groupby': '', |
| 179 'projects': []} |
| 180 self.use_cached_searches = True |
| 181 self.warnings = [] |
| 182 self.errors = template_helpers.EZTError() |
| 183 self.mode = None |
| 184 |
| 185 if hasattr(request, 'projectId'): |
| 186 self.project_name = request.projectId |
| 187 self.project = services.project.GetProjectByName( |
| 188 self.cnxn, self.project_name) |
| 189 self.params['projects'].append(self.project_name) |
| 190 self.config = services.config.GetProjectConfig( |
| 191 self.cnxn, self.project_id) |
| 192 if hasattr(request, 'additionalProject'): |
| 193 self.params['projects'].extend(request.additionalProject) |
| 194 self.params['projects'] = list(set(self.params['projects'])) |
| 195 if hasattr(request, 'issueId'): |
| 196 self.issue = services.issue.GetIssueByLocalID( |
| 197 self.cnxn, self.project_id, request.issueId) |
| 198 self.granted_perms = tracker_bizobj.GetGrantedPerms( |
| 199 self.issue, self.auth.effective_ids, self.config) |
| 200 if hasattr(request, 'userId'): |
| 201 self.viewed_username = request.userId.lower() |
| 202 if self.viewed_username == 'me': |
| 203 self.viewed_username = requester_email |
| 204 self.viewed_user_auth = AuthData.FromEmail( |
| 205 self.cnxn, self.viewed_username, services) |
| 206 elif hasattr(request, 'groupName'): |
| 207 self.viewed_username = request.groupName.lower() |
| 208 try: |
| 209 self.viewed_user_auth = AuthData.FromEmail( |
| 210 self.cnxn, self.viewed_username, services) |
| 211 except user_svc.NoSuchUserException: |
| 212 self.viewed_user_auth = None |
| 213 self.perms = permissions.GetPermissions( |
| 214 self.auth.user_pb, self.auth.effective_ids, self.project) |
| 215 |
| 216 # Build q. |
| 217 if hasattr(request, 'q') and request.q: |
| 218 self.params['q'] = request.q |
| 219 if hasattr(request, 'publishedMax') and request.publishedMax: |
| 220 self.params['q'] += ' opened<=%d' % request.publishedMax |
| 221 if hasattr(request, 'publishedMin') and request.publishedMin: |
| 222 self.params['q'] += ' opened>=%d' % request.publishedMin |
| 223 if hasattr(request, 'updatedMax') and request.updatedMax: |
| 224 self.params['q'] += ' modified<=%d' % request.updatedMax |
| 225 if hasattr(request, 'updatedMin') and request.updatedMin: |
| 226 self.params['q'] += ' modified>=%d' % request.updatedMin |
| 227 if hasattr(request, 'owner') and request.owner: |
| 228 self.params['q'] += ' owner:%s' % request.owner |
| 229 if hasattr(request, 'status') and request.status: |
| 230 self.params['q'] += ' status:%s' % request.status |
| 231 if hasattr(request, 'label') and request.label: |
| 232 self.params['q'] += ' label:%s' % request.label |
| 233 |
| 234 if hasattr(request, 'can') and request.can: |
| 235 if request.can == api_pb2_v1.CannedQuery.all: |
| 236 self.params['can'] = 1 |
| 237 elif request.can == api_pb2_v1.CannedQuery.new: |
| 238 self.params['can'] = 6 |
| 239 elif request.can == api_pb2_v1.CannedQuery.open: |
| 240 self.params['can'] = 2 |
| 241 elif request.can == api_pb2_v1.CannedQuery.owned: |
| 242 self.params['can'] = 3 |
| 243 elif request.can == api_pb2_v1.CannedQuery.reported: |
| 244 self.params['can'] = 4 |
| 245 elif request.can == api_pb2_v1.CannedQuery.starred: |
| 246 self.params['can'] = 5 |
| 247 elif request.can == api_pb2_v1.CannedQuery.to_verify: |
| 248 self.params['can'] = 7 |
| 249 else: # Endpoints should have caught this. |
| 250 raise InputException( |
| 251 'Canned query %s is not supported.', request.can) |
| 252 if hasattr(request, 'startIndex') and request.startIndex: |
| 253 self.params['start'] = request.startIndex |
| 254 if hasattr(request, 'maxResults') and request.maxResults: |
| 255 self.params['num'] = request.maxResults |
| 256 if hasattr(request, 'sort') and request.sort: |
| 257 self.params['sort'] = request.sort |
| 258 |
| 259 self.query_project_names = self.GetParam('projects') |
| 260 self.group_by_spec = self.GetParam('groupby') |
| 261 self.sort_spec = self.GetParam('sort') |
| 262 self.query = self.GetParam('q') |
| 263 self.can = self.GetParam('can') |
| 264 self.start = self.GetParam('start') |
| 265 self.num = self.GetParam('num') |
| 266 |
| 267 @property |
| 268 def project_id(self): |
| 269 return self.project.project_id if self.project else None |
| 270 |
| 271 def GetParam(self, query_param_name, default_value=None, |
| 272 _antitamper_re=None): |
| 273 return self.params.get(query_param_name, default_value) |
| 274 |
| 275 def GetPositiveIntParam(self, query_param_name, default_value=None): |
| 276 """Returns 0 if the user-provided value is less than 0.""" |
| 277 return max(self.GetParam(query_param_name, default_value=default_value), |
| 278 0) |
| 279 |
| 280 |
| 281 class MonorailRequest(object): |
| 282 """A class to hold information parsed from the HTTP request. |
| 283 |
| 284 The goal of MonorailRequest is to do almost all URL path and query string |
| 285 procesing in one place, which makes the servlet code simpler. |
| 286 |
| 287 Attributes: |
| 288 cnxn: connection to the SQL databases. |
| 289 logged_in_user_id: int user ID of the signed-in user, or None. |
| 290 effective_ids: set of signed-in user ID and all their user group IDs. |
| 291 user_pb: User object for the signed in user. |
| 292 project_name: string name of the current project. |
| 293 project_id: int ID of the current projet. |
| 294 viewed_username: string username of the user whose profile is being viewed. |
| 295 can: int "canned query" number to scope the user's search. |
| 296 num: int number of results to show per pagination page. |
| 297 start: int position in result set to show on this pagination page. |
| 298 etc: there are many more, all read-only. |
| 299 """ |
| 300 |
| 301 # pylint: disable=attribute-defined-outside-init |
| 302 def __init__(self, params=None): |
| 303 """Initialize the MonorailRequest object.""" |
| 304 self.form_overrides = {} |
| 305 if params: |
| 306 self.form_overrides.update(params) |
| 307 self.warnings = [] |
| 308 self.errors = template_helpers.EZTError() |
| 309 self.debug_enabled = False |
| 310 self.use_cached_searches = True |
| 311 self.cnxn = sql.MonorailConnection() |
| 312 |
| 313 self.auth = AuthData() # Authentication info for logged-in user |
| 314 |
| 315 self.project_name = None |
| 316 self.project = None |
| 317 |
| 318 self.viewed_username = None |
| 319 self.viewed_user_auth = AuthData() |
| 320 |
| 321 @property |
| 322 def project_id(self): |
| 323 return self.project.project_id if self.project else None |
| 324 |
| 325 def CleanUp(self): |
| 326 """Close the database connection so that the app does not run out.""" |
| 327 if self.cnxn: |
| 328 self.cnxn.Close() |
| 329 self.cnxn = None |
| 330 |
| 331 def ParseRequest(self, request, services, prof, do_user_lookups=True): |
| 332 """Parse tons of useful info from the given request object. |
| 333 |
| 334 Args: |
| 335 request: webapp2 Request object w/ path and query params. |
| 336 services: connections to backend servers including DB. |
| 337 prof: Profiler instance. |
| 338 do_user_lookups: Set to False to disable lookups during testing. |
| 339 """ |
| 340 with prof.Phase('basic parsing'): |
| 341 self.request = request |
| 342 self.current_page_url = request.url |
| 343 self.current_page_url_encoded = urllib.quote_plus(self.current_page_url) |
| 344 |
| 345 # Only accept a hostport from the request that looks valid. |
| 346 if not _HOSTPORT_RE.match(request.host): |
| 347 raise InputException('request.host looks funny: %r', request.host) |
| 348 |
| 349 logging.info('Request: %s', self.current_page_url) |
| 350 |
| 351 with prof.Phase('path parsing'): |
| 352 viewed_user_val, self.project_name = _ParsePathIdentifiers( |
| 353 self.request.path) |
| 354 self.viewed_username = _GetViewedEmail( |
| 355 viewed_user_val, self.cnxn, services) |
| 356 with prof.Phase('qs parsing'): |
| 357 self._ParseQueryParameters() |
| 358 with prof.Phase('overrides parsing'): |
| 359 self._ParseFormOverrides() |
| 360 |
| 361 if not self.project: # It can be already set in unit tests. |
| 362 self._LookupProject(services, prof) |
| 363 if do_user_lookups: |
| 364 if self.viewed_username: |
| 365 self._LookupViewedUser(services, prof) |
| 366 self._LookupLoggedInUser(services, prof) |
| 367 # TODO(jrobbins): re-implement HandleLurkerViewingSelf() |
| 368 |
| 369 prod_debug_allowed = self.perms.HasPerm( |
| 370 permissions.VIEW_DEBUG, self.auth.user_id, None) |
| 371 self.debug_enabled = (request.params.get('debug') and |
| 372 (settings.dev_mode or prod_debug_allowed)) |
| 373 # temporary option for perf testing on staging instance. |
| 374 if request.params.get('disable_cache'): |
| 375 if settings.dev_mode or 'staging' in request.host: |
| 376 self.use_cached_searches = False |
| 377 |
| 378 def _ParseQueryParameters(self): |
| 379 """Parse and convert all the query string params used in any servlet.""" |
| 380 self.start = self.GetPositiveIntParam('start', default_value=0) |
| 381 self.num = self.GetPositiveIntParam('num', default_value=100) |
| 382 # Prevent DoS attacks that try to make us serve really huge result pages. |
| 383 self.num = min(self.num, settings.max_artifact_search_results_per_page) |
| 384 |
| 385 self.invalidation_timestep = self.GetIntParam( |
| 386 'invalidation_timestep', default_value=0) |
| 387 |
| 388 self.continue_issue_id = self.GetIntParam( |
| 389 'continue_issue_id', default_value=0) |
| 390 self.redir = self.GetParam('redir') |
| 391 |
| 392 # Search scope, a.k.a., canned query ID |
| 393 # TODO(jrobbins): make configurable |
| 394 self.can = self.GetIntParam( |
| 395 'can', default_value=tracker_constants.OPEN_ISSUES_CAN) |
| 396 |
| 397 # Search query |
| 398 self.query = self.GetParam('q', default_value='').strip() |
| 399 |
| 400 # Sorting of search results (needed for result list and flipper) |
| 401 self.sort_spec = self.GetParam( |
| 402 'sort', default_value='', |
| 403 antitamper_re=framework_constants.SORTSPEC_RE) |
| 404 |
| 405 # Note: This is set later in request handling by ComputeColSpec(). |
| 406 self.col_spec = None |
| 407 |
| 408 # Grouping of search results (needed for result list and flipper) |
| 409 self.group_by_spec = self.GetParam( |
| 410 'groupby', default_value='', |
| 411 antitamper_re=framework_constants.SORTSPEC_RE) |
| 412 |
| 413 # For issue list and grid mode. |
| 414 self.cursor = self.GetParam('cursor') |
| 415 self.preview = self.GetParam('preview') |
| 416 self.mode = self.GetParam('mode', default_value='list') |
| 417 self.x = self.GetParam('x', default_value='') |
| 418 self.y = self.GetParam('y', default_value='') |
| 419 self.cells = self.GetParam('cells', default_value='ids') |
| 420 |
| 421 # For the dashboard and issue lists included in the dashboard. |
| 422 self.ajah = self.GetParam('ajah') # AJAH = Asychronous Javascript And HTML |
| 423 self.table_title = self.GetParam('table_title') |
| 424 self.panel_id = self.GetIntParam('panel') |
| 425 |
| 426 # For pagination of updates lists |
| 427 self.before = self.GetPositiveIntParam('before') |
| 428 self.after = self.GetPositiveIntParam('after') |
| 429 |
| 430 # For cron tasks and backend calls |
| 431 self.lower_bound = self.GetIntParam('lower_bound') |
| 432 self.upper_bound = self.GetIntParam('upper_bound') |
| 433 self.shard_id = self.GetIntParam('shard_id') |
| 434 |
| 435 # For specifying which objects to operate on |
| 436 self.local_id = self.GetIntParam('id') |
| 437 self.local_id_list = self.GetIntListParam('ids') |
| 438 self.seq = self.GetIntParam('seq') |
| 439 self.aid = self.GetIntParam('aid') |
| 440 self.specified_user_id = self.GetIntParam('u', default_value=0) |
| 441 self.specified_logged_in_user_id = self.GetIntParam( |
| 442 'logged_in_user_id', default_value=0) |
| 443 self.specified_me_user_id = self.GetIntParam( |
| 444 'me_user_id', default_value=0) |
| 445 self.specified_project = self.GetParam('project') |
| 446 self.specified_project_id = self.GetIntParam('project_id') |
| 447 self.query_project_names = self.GetListParam('projects', default_value=[]) |
| 448 self.template_name = self.GetParam('template') |
| 449 self.component_path = self.GetParam('component') |
| 450 self.field_name = self.GetParam('field') |
| 451 |
| 452 # For image attachments |
| 453 self.inline = bool(self.GetParam('inline')) |
| 454 self.thumb = bool(self.GetParam('thumb')) |
| 455 |
| 456 # For JS callbacks |
| 457 self.token = self.GetParam('token') |
| 458 self.starred = bool(self.GetIntParam('starred')) |
| 459 |
| 460 # For issue reindexing utility servlet |
| 461 self.auto_submit = self.GetParam('auto_submit') |
| 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 |
| 479 # For the people pages |
| 480 'initial_add_members': self.GetParam('add_members'), |
| 481 'initially_expanded_form': ezt.boolean(self.GetParam('expand_form')), |
| 482 |
| 483 # For user group admin pages |
| 484 'initial_name': (self.GetParam('group_name') or |
| 485 self.GetParam('proposed_project_name')), |
| 486 } |
| 487 |
| 488 # Only keep the overrides that were actually provided in the query string. |
| 489 self.form_overrides.update( |
| 490 (k, v) for (k, v) in allowed_overrides.iteritems() |
| 491 if v is not None) |
| 492 |
| 493 def _LookupViewedUser(self, services, prof): |
| 494 """Get information about the viewed user (if any) from the request.""" |
| 495 try: |
| 496 with prof.Phase('get viewed user, if any'): |
| 497 self.viewed_user_auth = AuthData.FromEmail( |
| 498 self.cnxn, self.viewed_username, services, autocreate=False) |
| 499 except user_svc.NoSuchUserException: |
| 500 logging.info('could not find user %r', self.viewed_username) |
| 501 webapp2.abort(404, 'user not found') |
| 502 |
| 503 if not self.viewed_user_auth.user_id: |
| 504 webapp2.abort(404, 'user not found') |
| 505 |
| 506 def _LookupProject(self, services, prof): |
| 507 """Get information about the current project (if any) from the request.""" |
| 508 with prof.Phase('get current project, if any'): |
| 509 if not self.project_name: |
| 510 logging.info('no project_name, so no project') |
| 511 else: |
| 512 self.project = services.project.GetProjectByName( |
| 513 self.cnxn, self.project_name) |
| 514 if not self.project: |
| 515 webapp2.abort(404, 'invalid project') |
| 516 |
| 517 def _LookupLoggedInUser(self, services, prof): |
| 518 """Get information about the signed-in user (if any) from the request.""" |
| 519 with prof.Phase('get user info, if any'): |
| 520 self.auth = AuthData.FromRequest(self.cnxn, services) |
| 521 self.me_user_id = (self.GetIntParam('me') or |
| 522 self.viewed_user_auth.user_id or self.auth.user_id) |
| 523 |
| 524 with prof.Phase('looking up signed in user permissions'): |
| 525 self.perms = permissions.GetPermissions( |
| 526 self.auth.user_pb, self.auth.effective_ids, self.project) |
| 527 |
| 528 def ComputeColSpec(self, config): |
| 529 """Set col_spec based on param, default in the config, or site default.""" |
| 530 if self.col_spec is not None: |
| 531 return # Already set. |
| 532 default_col_spec = '' |
| 533 if config: |
| 534 default_col_spec = config.default_col_spec |
| 535 |
| 536 col_spec = self.GetParam( |
| 537 'colspec', default_value=default_col_spec, |
| 538 antitamper_re=framework_constants.COLSPEC_RE) |
| 539 |
| 540 if not col_spec: |
| 541 # If col spec is still empty then default to the global col spec. |
| 542 col_spec = tracker_constants.DEFAULT_COL_SPEC |
| 543 |
| 544 self.col_spec = ' '.join(ParseColSpec(col_spec)) |
| 545 |
| 546 def PrepareForReentry(self, echo_data): |
| 547 """Expose the results of form processing as if it was a new GET. |
| 548 |
| 549 This method is called only when the user submits a form with invalid |
| 550 information which they are being asked to correct it. Updating the MR |
| 551 object allows the normal servlet get() method to populate the form with |
| 552 the entered values and error messages. |
| 553 |
| 554 Args: |
| 555 echo_data: dict of {page_data_key: value_to_reoffer, ...} that will |
| 556 override whatever HTML form values are nomally shown to the |
| 557 user when they initially view the form. This allows them to |
| 558 fix user input that was not valid. |
| 559 """ |
| 560 self.form_overrides.update(echo_data) |
| 561 |
| 562 def GetParam(self, query_param_name, default_value=None, |
| 563 antitamper_re=None): |
| 564 """Get a query parameter from the URL as a utf8 string.""" |
| 565 value = self.request.params.get(query_param_name) |
| 566 assert value is None or isinstance(value, unicode) |
| 567 using_default = value is None |
| 568 if using_default: |
| 569 value = default_value |
| 570 |
| 571 if antitamper_re and not antitamper_re.match(value): |
| 572 if using_default: |
| 573 logging.error('Default value fails antitamper for %s field: %s', |
| 574 query_param_name, value) |
| 575 else: |
| 576 logging.info('User seems to have tampered with %s field: %s', |
| 577 query_param_name, value) |
| 578 raise InputException() |
| 579 |
| 580 return value |
| 581 |
| 582 def GetIntParam(self, query_param_name, default_value=None): |
| 583 """Get an integer param from the URL or default.""" |
| 584 value = self.request.params.get(query_param_name) |
| 585 if value is None: |
| 586 return default_value |
| 587 |
| 588 try: |
| 589 return int(value) |
| 590 except (TypeError, ValueError): |
| 591 return default_value |
| 592 |
| 593 def GetPositiveIntParam(self, query_param_name, default_value=None): |
| 594 """Returns 0 if the user-provided value is less than 0.""" |
| 595 return max(self.GetIntParam(query_param_name, default_value=default_value), |
| 596 0) |
| 597 |
| 598 def GetListParam(self, query_param_name, default_value=None): |
| 599 """Get a list of strings from the URL or default.""" |
| 600 params = self.request.params.get(query_param_name) |
| 601 if params is None: |
| 602 return default_value |
| 603 if not params: |
| 604 return [] |
| 605 return params.split(',') |
| 606 |
| 607 def GetIntListParam(self, query_param_name, default_value=None): |
| 608 """Get a list of ints from the URL or default.""" |
| 609 param_list = self.GetListParam(query_param_name) |
| 610 if param_list is None: |
| 611 return default_value |
| 612 |
| 613 try: |
| 614 return [int(p) for p in param_list] |
| 615 except (TypeError, ValueError): |
| 616 return default_value |
| 617 |
| 618 |
| 619 def _ParsePathIdentifiers(path): |
| 620 """Parse out the workspace being requested (if any). |
| 621 |
| 622 Args: |
| 623 path: A string beginning with the request's path info. |
| 624 |
| 625 Returns: |
| 626 (viewed_user_val, project_name). |
| 627 """ |
| 628 viewed_user_val = None |
| 629 project_name = None |
| 630 |
| 631 # Strip off any query params |
| 632 split_path = path.lstrip('/').split('?')[0].split('/') |
| 633 |
| 634 if len(split_path) >= 2: |
| 635 if split_path[0] == 'p': |
| 636 project_name = split_path[1] |
| 637 if split_path[0] == 'u': |
| 638 viewed_user_val = urllib.unquote(split_path[1]) |
| 639 if split_path[0] == 'g': |
| 640 viewed_user_val = urllib.unquote(split_path[1]) |
| 641 |
| 642 return viewed_user_val, project_name |
| 643 |
| 644 |
| 645 def _GetViewedEmail(viewed_user_val, cnxn, services): |
| 646 """Returns the viewed user's email. |
| 647 |
| 648 Args: |
| 649 viewed_user_val: Could be either int (user_id) or str (email). |
| 650 cnxn: connection to the SQL database. |
| 651 services: Interface to all persistence storage backends. |
| 652 |
| 653 Returns: |
| 654 viewed_email |
| 655 """ |
| 656 if not viewed_user_val: |
| 657 return None |
| 658 |
| 659 try: |
| 660 viewed_userid = int(viewed_user_val) |
| 661 viewed_email = services.user.LookupUserEmail(cnxn, viewed_userid) |
| 662 if not viewed_email: |
| 663 logging.info('userID %s not found', viewed_userid) |
| 664 webapp2.abort(404, 'user not found') |
| 665 except ValueError: |
| 666 viewed_email = viewed_user_val |
| 667 |
| 668 return viewed_email |
| 669 |
| 670 |
| 671 def ParseColSpec(col_spec): |
| 672 """Split a string column spec into a list of column names. |
| 673 |
| 674 Args: |
| 675 col_spec: a unicode string containing a list of labels. |
| 676 |
| 677 Returns: |
| 678 A list of the extracted labels. Non-alphanumeric |
| 679 characters other than the period will be stripped from the text. |
| 680 """ |
| 681 return framework_constants.COLSPEC_COL_RE.findall(col_spec) |
| 682 |
| 683 |
| 684 class Error(Exception): |
| 685 """Base class for errors from this module.""" |
| 686 pass |
| 687 |
| 688 |
| 689 class InputException(Error): |
| 690 """Error in user input processing.""" |
| 691 pass |
OLD | NEW |