Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/framework/servlet.py b/framework/servlet.py
index b363095..15530a9 100644
--- a/framework/servlet.py
+++ b/framework/servlet.py
@@ -1,105 +1,64 @@
-# 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
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Base classes for Monorail Flask servlets.
-"""Base classes for Monorail servlets.
-
-This base class provides HTTP get() and post() methods that
-conveniently drive the process of parsing the request, checking base
-permissions, gathering common page information, gathering
-page-specific information, and adding on-page debugging information
-(when appropriate). Subclasses can simply implement the page-specific
-logic.
+This is derived from servlet.py
+This base class provides handler methods that conveniently drive
+the process of parsing the request, checking base permisssion,
+gathering common page information, gathering page-specific information,
+and adding on-page debugging information (when appropriate).
+Subclasses can simply implement the page-specific logic.
Summary of page classes:
- Servlet: abstract base class for all Monorail servlets.
- _ContextDebugItem: displays page_data elements for on-page debugging.
+ Servlet: abstract base class for all Monorail flask servlets.
"""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
import gc
-from six.moves import http_client
-import json
-import logging
import os
-import random
+import logging
+from six.moves import http_client
import time
-from six.moves import urllib
+from businesslogic import work_env
import ezt
+from features import features_bizobj, hotlist_views
+import flask
import httpagentparser
+from project import project_constants
+from mrproto import project_pb2
+from search import query2ast
+
+import settings
+from framework import alerts, exceptions, framework_helpers, urls
+from framework import framework_views, servlet_helpers
+from framework import framework_constants
+from framework import monorailrequest
+from framework import permissions
+from framework import ratelimiter
+from framework import template_helpers
+from framework import xsrf
from google.appengine.api import app_identity
from google.appengine.api import modules
from google.appengine.api import users
-from oauth2client.client import GoogleCredentials
-
-import webapp2
-
-import settings
-from businesslogic import work_env
-from features import savedqueries_helpers
-from features import features_bizobj
-from features import hotlist_views
-from framework import alerts
-from framework import exceptions
-from framework import framework_constants
-from framework import framework_helpers
-from framework import framework_views
-from framework import monorailrequest
-from framework import permissions
-from framework import ratelimiter
-from framework import servlet_helpers
-from framework import template_helpers
-from framework import urls
-from framework import xsrf
-from project import project_constants
-from proto import project_pb2
-from search import query2ast
from tracker import tracker_views
-
-from infra_libs import ts_mon
+from werkzeug import datastructures
NONCE_LENGTH = 32
if not settings.unit_test_mode:
import MySQLdb
-GC_COUNT = ts_mon.NonCumulativeDistributionMetric(
- 'monorail/servlet/gc_count',
- 'Count of objects in each generation tracked by the GC',
- [ts_mon.IntegerField('generation')])
-
-GC_EVENT_REQUEST = ts_mon.CounterMetric(
- 'monorail/servlet/gc_event_request',
- 'Counts of requests that triggered at least one GC event',
- [])
-
-# TODO(crbug/monorail:7084): Find a better home for this code.
-trace_service = None
-# TOD0(crbug/monorail:7082): Re-enable this once we have a solution that doesn't
-# inur clatency, or when we're actively using Cloud Tracing data.
-# if app_identity.get_application_id() != 'testing-app':
-# logging.warning('app id: %s', app_identity.get_application_id())
-# try:
-# credentials = GoogleCredentials.get_application_default()
-# trace_service = discovery.build(
-# 'cloudtrace', 'v1', credentials=credentials)
-# except Exception as e:
-# logging.warning('could not get trace service: %s', e)
-class Servlet(webapp2.RequestHandler):
- """Base class for all Monorail servlets.
+class Servlet(object):
+ """Base class for all Monorail flask servlets.
Defines a framework of methods that build up parts of the EZT page data.
Subclasses should override GatherPageData and/or ProcessFormData to
handle requests.
"""
-
- _MAIN_TAB_MODE = None # Normally overriden in subclasses to be one of these:
+ _MAIN_TAB_MODE = None # Normally overridden in subclasses to be one of these:
MAIN_TAB_NONE = 't0'
MAIN_TAB_DASHBOARD = 't1'
@@ -132,26 +91,16 @@
# we can allow an xhr-scoped XSRF token to be used to post to the page.
ALLOW_XHR = False
- # Most forms just ignore fields that have value "". Subclasses can override
- # if needed.
- KEEP_BLANK_FORM_VALUES = False
-
- # Most forms use regular forms, but subclasses that accept attached files can
- # override this to be True.
- MULTIPART_POST_BODY = False
-
# This value should not typically be overridden.
_TEMPLATE_PATH = framework_constants.TEMPLATE_PATH
- _PAGE_TEMPLATE = None # Normally overriden in subclasses.
+ _PAGE_TEMPLATE = None # Normally overridden in subclasses.
_ELIMINATE_BLANK_LINES = False
_MISSING_PERMISSIONS_TEMPLATE = 'sitewide/403-page.ezt'
- def __init__(self, request, response, services=None,
- content_type='text/html; charset=UTF-8'):
+ def __init__(self, services=None, content_type='text/html; charset=UTF-8'):
"""Load and parse the template, saving it for later use."""
- super(Servlet, self).__init__(request, response)
if self._PAGE_TEMPLATE: # specified in subclasses
template_path = self._TEMPLATE_PATH + self._PAGE_TEMPLATE
self.template = template_helpers.GetTemplate(
@@ -161,37 +110,40 @@
self._missing_permissions_template = template_helpers.MonorailTemplate(
self._TEMPLATE_PATH + self._MISSING_PERMISSIONS_TEMPLATE)
- self.services = services or self.app.config.get('services')
+ self.services = services or flask.current_app.config['services']
self.content_type = content_type
self.mr = None
+ # TODO: convert it to use self.request.path when we merge all flask together
+ self.request = flask.request
+ self.request_path = None
+ self.response = None
self.ratelimiter = ratelimiter.RateLimiter()
- def dispatch(self):
+ # pylint: disable=unused-argument
+ def handler(self, **kwargs):
"""Do common stuff then dispatch the request to get() or put() methods."""
+ self.response = flask.make_response()
handler_start_time = time.time()
+ logging.info('\n\n\n Flask Request handler: %r', self)
- logging.info('\n\n\nRequest handler: %r', self)
- count0, count1, count2 = gc.get_count()
- logging.info('gc counts: %d %d %d', count0, count1, count2)
- GC_COUNT.add(count0, {'generation': 0})
- GC_COUNT.add(count1, {'generation': 1})
- GC_COUNT.add(count2, {'generation': 2})
+ #TODO: add the ts_mon.NonCumulativeDistributionMetric
+ # count0, count1, count2 = gc.get_count()
+ # logging.info('gc counts: %d %d %d', count0, count1, count2)
+ # GC_COUNT.add(count0, {'generation': 0})
+ # GC_COUNT.add(count1, {'generation': 1})
+ # GC_COUNT.add(count2, {'generation': 2})
self.mr = monorailrequest.MonorailRequest(self.services)
-
- self.response.headers.add('Strict-Transport-Security',
- 'max-age=31536000; includeSubDomains')
+ # TODO: convert it to use self.request.path when we merge all flask together
+ self.request_path = self.request.base_url[len(self.request.host_url) - 1:]
+ self.response.headers.add(
+ 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
if 'X-Cloud-Trace-Context' in self.request.headers:
self.mr.profiler.trace_context = (
self.request.headers.get('X-Cloud-Trace-Context'))
- # TOD0(crbug/monorail:7082): Re-enable tracing.
- # if trace_service is not None:
- # self.mr.profiler.trace_service = trace_service
if self.services.cache_manager:
- # TODO(jrobbins): don't do this step if invalidation_timestep was
- # passed via the request and matches our last timestep
try:
with self.mr.profiler.Phase('distributed invalidation'):
self.services.cache_manager.DoDistributedInvalidation(self.mr.cnxn)
@@ -206,8 +158,8 @@
'templates/framework/database-maintenance.ezt',
eliminate_blank_lines=self._ELIMINATE_BLANK_LINES)
self.template.WriteResponse(
- self.response, page_data, content_type='text/html')
- return
+ self.response, page_data, content_type='text/html')
+ return self.response
try:
self.ratelimiter.CheckStart(self.request)
@@ -216,27 +168,33 @@
self.mr.ParseRequest(self.request, self.services)
self.response.headers['X-Frame-Options'] = 'SAMEORIGIN'
- webapp2.RequestHandler.dispatch(self)
+ if self.request.method == 'POST':
+ self.post()
+ elif self.request.method == 'GET':
+ self.get()
+
+ except exceptions.RedirectException as e:
+ return self.redirect(str(e))
except exceptions.NoSuchUserException as e:
- logging.warning('Trapped NoSuchUserException %s', e)
- self.abort(404, 'user not found')
+ logging.info('Trapped NoSuchUserException %s', e)
+ flask.abort(404, 'user not found')
except exceptions.NoSuchGroupException as e:
logging.warning('Trapped NoSuchGroupException %s', e)
- self.abort(404, 'user group not found')
+ flask.abort(404, 'user group not found')
except exceptions.InputException as e:
logging.info('Rejecting invalid input: %r', e)
- self.response.status = http_client.BAD_REQUEST
+ self.response.status_code = http_client.BAD_REQUEST
except exceptions.NoSuchProjectException as e:
logging.info('Rejecting invalid request: %r', e)
- self.response.status = http_client.NOT_FOUND
+ self.response.status_code = http_client.NOT_FOUND
except xsrf.TokenIncorrect as e:
- logging.info('Bad XSRF token: %r', e.message)
- self.response.status = http_client.BAD_REQUEST
+ logging.info('Bad XSRF token: %r', str(e))
+ self.response.status_code = http_client.BAD_REQUEST
except permissions.BannedUserException as e:
logging.warning('The user has been banned')
@@ -246,8 +204,8 @@
except ratelimiter.RateLimitExceeded as e:
logging.info('RateLimitExceeded Exception %s', e)
- self.response.status = http_client.BAD_REQUEST
- self.response.body = 'Slow your roll.'
+ self.response.status_code = http_client.BAD_REQUEST
+ self.response.set_data('Slow your roll.')
finally:
self.mr.CleanUp()
@@ -259,34 +217,17 @@
end_count0, end_count1, end_count2 = gc.get_count()
logging.info('gc counts: %d %d %d', end_count0, end_count1, end_count2)
- if (end_count0 < count0) or (end_count1 < count1) or (end_count2 < count2):
- GC_EVENT_REQUEST.increment()
+ # TODO: get the GC event back
+ # if (end_count0 < count0) or (end_count1 < count1) or(end_count2 < count2):
+ # GC_EVENT_REQUEST.increment()
if settings.enable_profiler_logging:
self.mr.profiler.LogStats()
- # TOD0(crbug/monorail:7082, crbug/monorail:7088): Re-enable this when we
- # have solved the latency, or when we really need the profiler data.
- # if self.mr.profiler.trace_context is not None:
- # try:
- # self.mr.profiler.ReportTrace()
- # except Exception as ex:
- # # We never want Cloud Tracing to cause a user-facing error.
- # logging.warning('Ignoring exception reporting Cloud Trace %s', ex)
+ return self.response
- def _AddHelpDebugPageData(self, page_data):
- with self.mr.profiler.Phase('help and debug data'):
- page_data.update(self.GatherHelpData(self.mr, page_data))
- page_data.update(self.GatherDebugData(self.mr, page_data))
-
- # pylint: disable=unused-argument
- def get(self, **kwargs):
- """Collect page-specific and generic info, then render the page.
-
- Args:
- Any path components parsed by webapp2 will be in kwargs, but we do
- our own parsing later anyway, so igore them for now.
- """
+ def get(self):
+ """Collect page-specific and generic info, then render the page."""
page_data = {}
nonce = framework_helpers.MakeRandomKey(length=NONCE_LENGTH)
try:
@@ -298,44 +239,52 @@
user_agent_str = self.mr.request.headers.get('User-Agent', '')
ua = httpagentparser.detect(user_agent_str)
browser, browser_major_version = 'Unknown browser', 0
- if ua.has_key('browser'):
+ if 'browser' in ua:
browser = ua['browser']['name']
try:
browser_major_version = int(ua['browser']['version'].split('.')[0])
except ValueError:
- logging.warn('Could not parse version: %r', ua['browser']['version'])
+ logging.warning(
+ 'Could not parse version: %r', ua['browser']['version'])
except KeyError:
- logging.warn('No browser version defined in user agent.')
+ logging.warning('No browser version defined in user agent.')
csp_supports_report_sample = (
- (browser == 'Chrome' and browser_major_version >= 59) or
- (browser == 'Opera' and browser_major_version >= 46))
+ (browser == 'Chrome' and browser_major_version >= 59) or
+ (browser == 'Opera' and browser_major_version >= 46))
version_base = servlet_helpers.VersionBaseURL(self.mr.request)
- self.response.headers.add(csp_header,
- ("default-src %(scheme)s ; "
- "script-src"
- " %(rep_samp)s" # Report 40 chars of any inline violation.
- " 'unsafe-inline'" # Only counts in browsers that lack CSP2.
- " 'strict-dynamic'" # Allows <script nonce> to load more.
- " %(version_base)s/static/dist/"
- " 'self' 'nonce-%(nonce)s'; "
- "child-src 'none'; "
- "frame-src accounts.google.com" # All used by gapi.js auth.
- " content-issuetracker.corp.googleapis.com"
- " login.corp.google.com up.corp.googleapis.com"
- # Used by Google Feedback
- " feedback.googleusercontent.com"
- " www.google.com; "
- "img-src %(scheme)s data: blob: ; "
- "style-src %(scheme)s 'unsafe-inline'; "
- "object-src 'none'; "
- "base-uri 'self'; " # Used by Google Feedback
- "report-uri /csp.do" % {
- 'nonce': nonce,
- 'scheme': csp_scheme,
- 'rep_samp': "'report-sample'" if csp_supports_report_sample else '',
- 'version_base': version_base,
- }))
+ self.response.headers.add(
+ csp_header,
+ (
+ "default-src %(scheme)s ; "
+ "script-src"
+ " %(rep_samp)s" # Report 40 chars of any inline violation.
+ " 'unsafe-inline'" # Only counts in browsers that lack CSP2.
+ " 'strict-dynamic'" # Allows <script nonce> to load more.
+ " %(version_base)s/static/dist/"
+ " 'self' 'nonce-%(nonce)s'; "
+ "child-src 'none'; "
+ "frame-src accounts.google.com" # All used by gapi.js auth.
+ " content-issuetracker.corp.googleapis.com"
+ " login.corp.google.com up.corp.googleapis.com"
+ # Used by Google Feedback
+ " feedback.googleusercontent.com"
+ " www.google.com; "
+ "img-src %(scheme)s data: blob: ; "
+ "style-src %(scheme)s 'unsafe-inline'; "
+ "object-src 'none'; "
+ "base-uri 'self'; " # Used by Google Feedback
+ "report-uri /csp.do" % {
+ 'nonce':
+ nonce,
+ 'scheme':
+ csp_scheme,
+ 'rep_samp':
+ "'report-sample'" if csp_supports_report_sample else '',
+ 'version_base':
+ version_base,
+ }))
+ # add the function to get data and render page
page_data.update(self._GatherFlagData(self.mr))
# Page-specific work happens in this call.
@@ -352,12 +301,12 @@
# meaningful during fuzzing. For more context see
# https://bugs.chromium.org/p/monorail/issues/detail?id=659
logging.warning('Trapped NotImplementedError %s', e)
- self.abort(404, 'invalid page')
+ flask.abort(404, 'invalid page')
except query2ast.InvalidQueryError as e:
logging.warning('Trapped InvalidQueryError: %s', e)
logging.exception(e)
- msg = e.message if e.message else 'invalid query'
- self.abort(400, msg)
+ msg = str(e) if str(e) else 'invalid query'
+ flask.abort(400, msg)
except permissions.PermissionException as e:
logging.warning('Trapped PermissionException %s', e)
logging.warning('mr.auth.user_id is %s', self.mr.auth.user_id)
@@ -365,12 +314,12 @@
logging.warning('mr.perms is %s', self.mr.perms)
if not self.mr.auth.user_id:
# If not logged in, let them log in
- url = servlet_helpers.SafeCreateLoginURL(self.mr)
- self.redirect(url, abort=True)
+ login_url = servlet_helpers.SafeCreateLoginURL(self.mr)
+ raise exceptions.RedirectException(login_url)
else:
# Display the missing permissions template.
page_data = {
- 'reason': e.message,
+ 'reason': str(e),
'http_response_code': http_client.FORBIDDEN,
}
with self.mr.profiler.Phase('gather base data'):
@@ -379,61 +328,34 @@
self._missing_permissions_template.WriteResponse(
self.response, page_data, content_type=self.content_type)
- def GetTemplate(self, _page_data):
- """Get the template to use for writing the http response.
-
- Defaults to self.template. This method can be overwritten in subclasses
- to allow dynamic template selection based on page_data.
-
- Args:
- _page_data: A dict of data for ezt rendering, containing base ezt
- data, page data, and debug data.
-
- Returns:
- The template to be used for writing the http response.
- """
- return self.template
-
- def _GatherFlagData(self, mr):
- page_data = {
- 'project_stars_enabled': ezt.boolean(
- settings.enable_project_stars),
- 'user_stars_enabled': ezt.boolean(settings.enable_user_stars),
- 'can_create_project': ezt.boolean(
- permissions.CanCreateProject(mr.perms)),
- 'can_create_group': ezt.boolean(
- permissions.CanCreateGroup(mr.perms)),
- }
-
- return page_data
-
- def _RenderResponse(self, page_data):
- logging.info('rendering response len(page_data) is %r', len(page_data))
- self.GetTemplate(page_data).WriteResponse(
- self.response, page_data, content_type=self.content_type)
-
- def ProcessFormData(self, mr, post_data):
- """Handle form data and redirect appropriately.
-
- Args:
- mr: commonly used info parsed from the request.
- post_data: HTML form data from the request.
-
- Returns:
- String URL to redirect the user to, or None if response was already sent.
- """
- raise servlet_helpers.MethodNotSupportedError()
-
- def post(self, **kwargs):
- """Parse the request, check base perms, and call form-specific code."""
+ def post(self):
+ logging.info('process post request')
try:
# Page-specific work happens in this call.
self._DoFormProcessing(self.request, self.mr)
except permissions.PermissionException as e:
logging.warning('Trapped permission-related exception "%s".', e)
- # TODO(jrobbins): can we do better than an error page? not much.
- self.response.status = http_client.BAD_REQUEST
+ self.response.status_code = http_client.BAD_REQUEST
+
+ def _RenderResponse(self, page_data):
+ logging.info('rendering response len(page_data) is %r', len(page_data))
+ self.template.WriteResponse(
+ self.response, page_data, content_type=self.content_type)
+
+ def _GatherFlagData(self, mr):
+ page_data = {
+ 'project_stars_enabled':
+ ezt.boolean(settings.enable_project_stars),
+ 'user_stars_enabled':
+ ezt.boolean(settings.enable_user_stars),
+ 'can_create_project':
+ ezt.boolean(permissions.CanCreateProject(mr.perms)),
+ 'can_create_group':
+ ezt.boolean(permissions.CanCreateGroup(mr.perms)),
+ }
+
+ return page_data
def _DoCommonRequestProcessing(self, request, mr):
"""Do common processing dependent on having the user and project pbs."""
@@ -441,11 +363,15 @@
self._CheckForMovedProject(mr, request)
self.AssertBasePermission(mr)
+ # pylint: disable=unused-argument
def _DoPageProcessing(self, mr, nonce):
"""Do user lookups and gather page-specific ezt data."""
with mr.profiler.Phase('common request data'):
+
self._DoCommonRequestProcessing(self.request, mr)
+
self._MaybeRedirectToBrandedDomain(self.request, mr.project_name)
+
page_data = self.GatherBaseData(mr, nonce)
with mr.profiler.Phase('page processing'):
@@ -463,123 +389,52 @@
if self.CHECK_SECURITY_TOKEN:
try:
xsrf.ValidateToken(
- request.POST.get('token'), mr.auth.user_id, request.path)
+ request.values.get('token'), mr.auth.user_id, self.request_path)
except xsrf.TokenIncorrect as err:
if self.ALLOW_XHR:
- xsrf.ValidateToken(request.POST.get('token'), mr.auth.user_id, 'xhr')
+ xsrf.ValidateToken(
+ request.values.get('token'), mr.auth.user_id, 'xhr')
else:
raise err
- redirect_url = self.ProcessFormData(mr, request.POST)
+ form_values = datastructures.MultiDict(request.values)
+ form_values.update(request.files)
+ redirect_url = self.ProcessFormData(mr, form_values)
# Most forms redirect the user to a new URL on success. If no
# redirect_url was returned, the form handler must have already
# sent a response. E.g., bounced the user back to the form with
- # invalid form fields higlighted.
+ # invalid form fields highlighted.
if redirect_url:
- self.redirect(redirect_url, abort=True)
+ raise exceptions.RedirectException(redirect_url)
else:
- assert self.response.body
+ assert self.response.response
- def _CheckForMovedProject(self, mr, request):
- """If the project moved, redirect there or to an informational page."""
- if not mr.project:
- return # We are on a site-wide or user page.
- if not mr.project.moved_to:
- return # This project has not moved.
- admin_url = '/p/%s%s' % (mr.project_name, urls.ADMIN_META)
- if request.path.startswith(admin_url):
- return # It moved, but we are near the page that can un-move it.
-
- logging.info('project %s has moved: %s', mr.project.project_name,
- mr.project.moved_to)
-
- moved_to = mr.project.moved_to
- if project_constants.RE_PROJECT_NAME.match(moved_to):
- # Use the redir query parameter to avoid redirect loops.
- if mr.redir is None:
- url = framework_helpers.FormatMovedProjectURL(mr, moved_to)
- if '?' in url:
- url += '&redir=1'
- else:
- url += '?redir=1'
- logging.info('trusted move to a new project on our site')
- self.redirect(url, abort=True)
-
- logging.info('not a trusted move, will display link to user to click')
- # Attach the project name as a url param instead of generating a /p/
- # link to the destination project.
- url = framework_helpers.FormatAbsoluteURL(
- mr, urls.PROJECT_MOVED,
- include_project=False, copy_params=False, project=mr.project_name)
- self.redirect(url, abort=True)
-
- def _MaybeRedirectToBrandedDomain(self, request, project_name):
- """If we are live and the project should be branded, check request host."""
- if request.params.get('redir'):
- return # Avoid any chance of a redirect loop.
- if not project_name:
- return
- needed_domain = framework_helpers.GetNeededDomain(
- project_name, request.host)
- if not needed_domain:
- return
-
- url = 'https://%s%s' % (needed_domain, request.path_qs)
- if '?' in url:
- url += '&redir=1'
- else:
- url += '?redir=1'
- logging.info('branding redirect to url %r', url)
- self.redirect(url, abort=True)
-
- def CheckPerm(self, mr, perm, art=None, granted_perms=None):
- """Return True if the user can use the requested permission."""
- return servlet_helpers.CheckPerm(
- mr, perm, art=art, granted_perms=granted_perms)
-
- def MakePagePerms(self, mr, art, *perm_list, **kwargs):
- """Make an EZTItem with a set of permissions needed in a given template.
+ def ProcessFormData(self, mr, post_data):
+ """Handle form data and redirect appropriately.
Args:
mr: commonly used info parsed from the request.
- art: a project artifact, such as an issue.
- *perm_list: any number of permission names that are referenced
- in the EZT template.
- **kwargs: dictionary that may include 'granted_perms' list of permissions
- granted to the current user specifically on the current page.
+ post_data: HTML form data from the request.
Returns:
- An EZTItem with one attribute for each permission and the value
- of each attribute being an ezt.boolean(). True if the user
- is permitted to do that action on the given artifact, or
- False if not.
+ String URL to redirect the user to, or None if response was already sent.
"""
- granted_perms = kwargs.get('granted_perms')
- page_perms = template_helpers.EZTItem()
- for perm in perm_list:
- setattr(
- page_perms, perm,
- ezt.boolean(self.CheckPerm(
- mr, perm, art=art, granted_perms=granted_perms)))
+ raise servlet_helpers.MethodNotSupportedError()
- return page_perms
+ def _FormHandlerURL(self, path):
+ """Return the form handler for the main form on a page."""
+ if path.endswith('/'):
+ return path + 'edit.do'
+ elif path.endswith('.do'):
+ return path # This happens as part of PleaseCorrect().
+ else:
+ return path + '.do'
- def AssertBasePermission(self, mr):
- """Make sure that the logged in user has permission to view this page.
-
- Subclasses should call super, then check additional permissions
- and raise a PermissionException if the user is not authorized to
- do something.
-
- Args:
- mr: commonly used info parsed from the request.
-
- Raises:
- PermissionException: If the user does not have permisssion to view
- the current page.
- """
- servlet_helpers.AssertBasePermission(mr)
+ # pylint: disable=unused-argument
+ def GatherPageData(self, mr):
+ """Return a dict of page-specific ezt data."""
+ raise servlet_helpers.MethodNotSupportedError()
def GatherBaseData(self, mr, nonce):
"""Return a dict of info used on almost all pages."""
@@ -601,10 +456,9 @@
is_project_starred = False
project_view = None
if mr.project:
- if permissions.UserCanViewProject(
- mr.auth.user_pb, mr.auth.effective_ids, mr.project):
+ if permissions.UserCanViewProject(mr.auth.user_pb,
+ mr.auth.effective_ids, mr.project):
is_project_starred = we.IsProjectStarred(mr.project_id)
- # TODO(jrobbins): should this be a ProjectView?
project_view = template_helpers.PBProxy(mr.project)
grid_x_attr = None
@@ -616,8 +470,9 @@
features_bizobj.UsersInvolvedInHotlists([mr.hotlist]))
hotlist_view = hotlist_views.HotlistView(
mr.hotlist, mr.perms, mr.auth, mr.viewed_user_auth.user_id,
- users_by_id, self.services.hotlist_star.IsItemStarredBy(
- mr.cnxn, mr.hotlist.hotlist_id, mr.auth.user_id))
+ users_by_id,
+ self.services.hotlist_star.IsItemStarredBy(
+ mr.cnxn, mr.hotlist.hotlist_id, mr.auth.user_id))
grid_x_attr = mr.x.lower()
grid_y_attr = mr.y.lower()
@@ -790,10 +645,6 @@
None, # First part of page title
'title_summary':
None, # Appended to title on artifact detail pages
-
- # TODO(jrobbins): make sure that the templates use
- # project_read_only for project-mutative actions and if any
- # uses of read_only remain.
'project_read_only':
ezt.boolean(project_read_only),
'site_read_only':
@@ -836,30 +687,22 @@
mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
# Always add other anti-xsrf tokens when the user is logged in.
if mr.auth.user_id:
- form_token_path = self._FormHandlerURL(mr.request.path)
+ form_token_path = self._FormHandlerURL(mr.request_path)
base_data['form_token'] = xsrf.GenerateToken(
- mr.auth.user_id, form_token_path)
+ mr.auth.user_id, form_token_path)
base_data['form_token_path'] = form_token_path
return base_data
- def _FormHandlerURL(self, path):
- """Return the form handler for the main form on a page."""
- if path.endswith('/'):
- return path + 'edit.do'
- elif path.endswith('.do'):
- return path # This happens as part of PleaseCorrect().
- else:
- return path + '.do'
-
- def GatherPageData(self, mr):
- """Return a dict of page-specific ezt data."""
- raise servlet_helpers.MethodNotSupportedError()
+ def _AddHelpDebugPageData(self, page_data):
+ with self.mr.profiler.Phase('help and debug data'):
+ page_data.update(self.GatherHelpData(self.mr, page_data))
+ page_data.update(self.GatherDebugData(self.mr, page_data))
# pylint: disable=unused-argument
def GatherHelpData(self, mr, page_data):
"""Return a dict of values to drive on-page user help.
-
+ Subclasses can override this function
Args:
mr: common information parsed from the HTTP request.
page_data: Dictionary of base and page template data.
@@ -870,13 +713,12 @@
help_data = {
'cue': None, # for cues.ezt
'account_cue': None, # for cues.ezt
- }
+ }
dismissed = []
if mr.auth.user_pb:
with work_env.WorkEnv(mr, self.services) as we:
userprefs = we.GetUserPrefs(mr.auth.user_id)
- dismissed = [
- pv.name for pv in userprefs.prefs if pv.value == 'true']
+ dismissed = [pv.name for pv in userprefs.prefs if pv.value == 'true']
if (mr.auth.user_pb.vacation_message and
'you_are_on_vacation' not in dismissed):
help_data['cue'] = 'you_are_on_vacation'
@@ -896,11 +738,12 @@
"""Return debugging info for display at the very bottom of the page."""
if mr.debug_enabled:
debug = [servlet_helpers.ContextDebugCollection('Page data', page_data)]
+ debug = [('none', 'recorded')]
return {
'dbg': 'on',
'debug': debug,
'profiler': mr.profiler,
- }
+ }
else:
if '?' in mr.current_page_url:
debug_url = mr.current_page_url + '&debug=1'
@@ -911,7 +754,117 @@
'debug_uri': debug_url,
'dbg': 'off',
'debug': [('none', 'recorded')],
- }
+ }
+
+ def _CheckForMovedProject(self, mr, request):
+ """If the project moved, redirect there or to an informational page."""
+ if not mr.project:
+ return # We are on a site-wide or user page.
+ if not mr.project.moved_to:
+ return # This project has not moved.
+ admin_url = '/p/%s%s' % (mr.project_name, urls.ADMIN_META)
+ if self.request_path.startswith(admin_url):
+ return # It moved, but we are near the page that can un-move it.
+
+ logging.info(
+ 'project %s has moved: %s', mr.project.project_name,
+ mr.project.moved_to)
+
+ moved_to = mr.project.moved_to
+ if project_constants.RE_PROJECT_NAME.match(moved_to):
+ # Use the redir query parameter to avoid redirect loops.
+ if mr.redir is None:
+ url = framework_helpers.FormatMovedProjectURL(mr, moved_to)
+ if '?' in url:
+ url += '&redir=1'
+ else:
+ url += '?redir=1'
+ logging.info('trusted move to a new project on our site')
+ self.redirect(url, abort=True)
+
+ logging.info('not a trusted move, will display link to user to click')
+ # Attach the project name as a url param instead of generating a /p/
+ # link to the destination project.
+ url = framework_helpers.FormatAbsoluteURL(
+ mr,
+ urls.PROJECT_MOVED,
+ include_project=False,
+ copy_params=False,
+ project=mr.project_name)
+ self.redirect(url, abort=True)
+
+ def _MaybeRedirectToBrandedDomain(self, request, project_name):
+ """If we are live and the project should be branded, check request host."""
+ if request.values.get('redir'):
+ return # Avoid any chance of a redirect loop.
+ if not project_name:
+ return
+ needed_domain = framework_helpers.GetNeededDomain(
+ project_name, request.host)
+ if not needed_domain:
+ return
+
+ url = 'https://%s%s' % (needed_domain, request.full_path)
+ if '?' in url:
+ url += '&redir=1'
+ else:
+ url += '?redir=1'
+ logging.info('branding redirect to url %r', url)
+ self.redirect(url, abort=True)
+
+ def AssertBasePermission(self, mr):
+ """Make sure that the logged in user has permission to view this page.
+
+ Subclasses should call super, then check additional permissions
+ and raise a PermissionException if the user is not authorized to
+ do something.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Raises:
+ PermissionException: If the user does not have permisssion to view
+ the current page.
+ """
+ servlet_helpers.AssertBasePermission(mr)
+
+ def CheckPerm(self, mr, perm, art=None, granted_perms=None):
+ """Return True if the user can use the requested permission."""
+ return servlet_helpers.CheckPerm(
+ mr, perm, art=art, granted_perms=granted_perms)
+
+ def MakePagePerms(self, mr, art, *perm_list, **kwargs):
+ """Make an EZTItem with a set of permissions needed in a given template.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ art: a project artifact, such as an issue.
+ *perm_list: any number of permission names that are referenced
+ in the EZT template.
+ **kwargs: dictionary that may include 'granted_perms' list of permissions
+ granted to the current user specifically on the current page.
+
+ Returns:
+ An EZTItem with one attribute for each permission and the value
+ of each attribute being an ezt.boolean(). True if the user
+ is permitted to do that action on the given artifact, or
+ False if not.
+ """
+ granted_perms = kwargs.get('granted_perms')
+ page_perms = template_helpers.EZTItem()
+ for perm in perm_list:
+ setattr(
+ page_perms, perm,
+ ezt.boolean(
+ self.CheckPerm(mr, perm, art=art, granted_perms=granted_perms)))
+
+ return page_perms
+
+ def redirect(self, url, abort=False):
+ if abort:
+ return flask.redirect(url, code=302)
+ else:
+ return flask.redirect(url)
def PleaseCorrect(self, mr, **echo_data):
"""Show the same form again so that the user can correct their input."""
@@ -927,3 +880,6 @@
now - framework_constants.VISIT_RESOLUTION):
user_pb.last_visit_timestamp = now
self.services.user.UpdateUser(mr.cnxn, user_pb.user_id, user_pb)
+
+ def abort(self, code, context=""):
+ return flask.abort(code, context)