Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/framework/alerts.py b/framework/alerts.py
index 1d24f77..ca2ab2d 100644
--- a/framework/alerts.py
+++ b/framework/alerts.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Helpers for showing alerts at the top of the page.
diff --git a/framework/authdata.py b/framework/authdata.py
index 3c1bee9..34e1ca3 100644
--- a/framework/authdata.py
+++ b/framework/authdata.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes to hold information parsed from a request.
"""
@@ -11,7 +10,7 @@
from google.appengine.api import users
-from proto import user_pb2
+from mrproto import user_pb2
from framework import framework_bizobj
from framework import framework_views
diff --git a/framework/banned.py b/framework/banned.py
index 209a715..5a4cd07 100644
--- a/framework/banned.py
+++ b/framework/banned.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A class to display the a message explaining that the user has been banned.
@@ -18,11 +17,11 @@
import ezt
-from framework import flaskservlet, permissions
+from framework import permissions
from framework import servlet
-class Banned(flaskservlet.FlaskServlet):
+class Banned(servlet.Servlet):
"""The Banned page shows a message explaining that the user is banned."""
_PAGE_TEMPLATE = 'framework/banned-page.ezt'
diff --git a/framework/clientmon.py b/framework/clientmon.py
index fd10684..06e0266 100644
--- a/framework/clientmon.py
+++ b/framework/clientmon.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A class to log client-side javascript error reports.
@@ -19,7 +18,7 @@
from infra_libs import ts_mon
-class ClientMonitor(jsonfeed.FlaskJsonFeed):
+class ClientMonitor(jsonfeed.JsonFeed):
"""JSON feed to track client side js errors in ts_mon."""
js_errors = ts_mon.CounterMetric('frontend/js_errors',
diff --git a/framework/cloud_tasks_helpers.py b/framework/cloud_tasks_helpers.py
index bd9b7f9..89954e3 100644
--- a/framework/cloud_tasks_helpers.py
+++ b/framework/cloud_tasks_helpers.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A helper module for interfacing with google cloud tasks.
@@ -12,6 +12,7 @@
from __future__ import print_function
import logging
+import six
from six.moves import urllib
from google.api_core import exceptions
@@ -21,7 +22,8 @@
if not settings.unit_test_mode:
import grpc
- from google.cloud import tasks
+ from google.cloud import tasks_v2
+ from google.cloud.tasks_v2.services import cloud_tasks
_client = None
# Default exponential backoff retry config for enqueueing, not to be confused
@@ -35,10 +37,11 @@
global _client
if not _client:
if settings.local_mode:
- _client = tasks.CloudTasksClient(
+ transport = cloud_tasks.transports.CloudTasksGrpcTransport(
channel=grpc.insecure_channel(settings.CLOUD_TASKS_EMULATOR_ADDRESS))
+ _client = tasks_v2.CloudTasksClient(transport=transport)
else:
- _client = tasks.CloudTasksClient()
+ _client = tasks_v2.CloudTasksClient()
return _client
@@ -74,7 +77,7 @@
target = task.get('app_engine_http_request').get('relative_uri')
kwargs.setdefault('retry', _DEFAULT_RETRY)
logging.info('Enqueueing %s task to %s', target, parent)
- return client.create_task(parent, task, **kwargs)
+ return client.create_task(parent=parent, task=task, **kwargs)
def generate_simple_task(url, params):
@@ -91,7 +94,7 @@
'app_engine_http_request':
{
'relative_uri': url,
- 'body': urllib.parse.urlencode(params),
+ 'body': six.ensure_binary(urllib.parse.urlencode(params)),
'headers': {
'Content-type': 'application/x-www-form-urlencoded'
}
diff --git a/framework/csp_report.py b/framework/csp_report.py
index 4b6f29e..08c061d 100644
--- a/framework/csp_report.py
+++ b/framework/csp_report.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Servlet for Content Security Policy violation reporting.
See http://www.html5rocks.com/en/tutorials/security/content-security-policy/
@@ -18,3 +17,4 @@
def postCsp():
"""CSPReportPage serves CSP violation reports."""
logging.error('CSP Violation: %s' % flask.request.get_data(as_text=True))
+ return ''
diff --git a/framework/csv_helpers.py b/framework/csv_helpers.py
index 3dd10c7..ac6509f 100644
--- a/framework/csv_helpers.py
+++ b/framework/csv_helpers.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Helper functions for creating CSV pagedata."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
+import six
import types
from framework import framework_helpers
@@ -63,7 +63,7 @@
"""Return a version of string S that is safe as part of a CSV file."""
if s is None:
return ''
- if isinstance(s, types.StringTypes):
+ if isinstance(s, six.string_types):
s = s.strip().replace('"', '""')
# Prefix any formula cells because some spreadsheets have built-in
# formila functions that can actually have side-effects on the user's
diff --git a/framework/deleteusers.py b/framework/deleteusers.py
index 015fad4..5c16189 100644
--- a/framework/deleteusers.py
+++ b/framework/deleteusers.py
@@ -1,6 +1,6 @@
-# Copyright 2019 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.
+# Copyright 2019 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Cron and task handlers for syncing with wipeoute-lite and deleting users."""
@@ -32,7 +32,7 @@
return credentials.authorize(httplib2.Http(timeout=60))
-class WipeoutSyncCron(jsonfeed.FlaskInternalTask):
+class WipeoutSyncCron(jsonfeed.InternalTask):
"""Enqueue tasks for sending user lists to wipeout-lite and deleting deleted
users fetched from wipeout-lite."""
@@ -65,7 +65,7 @@
return self.handler(**kwargs)
-class SendWipeoutUserListsTask(jsonfeed.FlaskInternalTask):
+class SendWipeoutUserListsTask(jsonfeed.InternalTask):
"""Sends a batch of monorail users to wipeout-lite."""
def HandleRequest(self, mr):
@@ -94,7 +94,7 @@
return self.handler(**kwargs)
-class DeleteWipeoutUsersTask(jsonfeed.FlaskInternalTask):
+class DeleteWipeoutUsersTask(jsonfeed.InternalTask):
"""Fetches deleted users from wipeout-lite and enqueues tasks to delete
those users from Monorail's DB."""
@@ -132,7 +132,7 @@
return self.handler(**kwargs)
-class DeleteUsersTask(jsonfeed.FlaskInternalTask):
+class DeleteUsersTask(jsonfeed.InternalTask):
"""Deletes users from Monorail's DB."""
def HandleRequest(self, mr):
diff --git a/framework/emailfmt.py b/framework/emailfmt.py
index 2933fea..e14075c 100644
--- a/framework/emailfmt.py
+++ b/framework/emailfmt.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Functions that format or parse email messages in Monorail.
@@ -13,13 +12,18 @@
from __future__ import division
from __future__ import absolute_import
+import hashlib
import hmac
import logging
import re
-import rfc822
import six
+if six.PY2:
+ import rfc822
+else:
+ import email.utils
+
from google.appengine.api import app_identity
import settings
@@ -116,7 +120,10 @@
def _ExtractAddrs(header_value):
"""Given a message header value, return email address found there."""
- friendly_addr_pairs = list(rfc822.AddressList(header_value))
+ if six.PY2:
+ friendly_addr_pairs = list(rfc822.AddressList(header_value))
+ else:
+ friendly_addr_pairs = email.utils.getaddresses([header_value])
return [addr for _friendly, addr in friendly_addr_pairs]
@@ -230,11 +237,15 @@
if isinstance(normalized_subject, six.text_type):
normalized_subject = normalized_subject.encode('utf-8')
mail_hmac_key = secrets_svc.GetEmailKey()
+ to_addr_hash = hmac.new(
+ mail_hmac_key, six.ensure_binary(to_addr),
+ digestmod=hashlib.md5).hexdigest()
+ subject_hash = hmac.new(
+ mail_hmac_key,
+ six.ensure_binary(normalized_subject),
+ digestmod=hashlib.md5).hexdigest()
return '<0=%s=%s=%s@%s>' % (
- hmac.new(mail_hmac_key, to_addr).hexdigest(),
- hmac.new(mail_hmac_key, normalized_subject).hexdigest(),
- from_addr.split('@')[0],
- MailDomain())
+ to_addr_hash, subject_hash, from_addr.split('@')[0], MailDomain())
def GetReferences(to_addr, subject, seq_num, project_from_addr):
diff --git a/framework/exceptions.py b/framework/exceptions.py
index 51c9951..2024f51 100644
--- a/framework/exceptions.py
+++ b/framework/exceptions.py
@@ -1,7 +1,6 @@
-# Copyright 2017 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 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Exception classes used throughout monorail.
"""
@@ -182,3 +181,8 @@
class OverAttachmentQuota(Error):
"""Project will exceed quota if the current operation is allowed."""
pass
+
+
+class RedirectException(Error):
+ """Page need to Redirect to new url."""
+ pass
diff --git a/framework/excessiveactivity.py b/framework/excessiveactivity.py
index 5506de3..87477b8 100644
--- a/framework/excessiveactivity.py
+++ b/framework/excessiveactivity.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A class to display the an error page for excessive activity.
@@ -12,10 +11,10 @@
from __future__ import division
from __future__ import absolute_import
-from framework import flaskservlet
+from framework import servlet
-class ExcessiveActivity(flaskservlet.FlaskServlet):
+class ExcessiveActivity(servlet.Servlet):
"""ExcessiveActivity page shows an error message."""
_PAGE_TEMPLATE = 'framework/excessive-activity-page.ezt'
diff --git a/framework/filecontent.py b/framework/filecontent.py
index 15d2940..7e79643 100644
--- a/framework/filecontent.py
+++ b/framework/filecontent.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Utility routines for dealing with MIME types and decoding text files."""
@@ -11,6 +10,7 @@
import itertools
import logging
+import six
from framework import framework_constants
@@ -160,7 +160,7 @@
# If the string can be decoded as utf-8, we treat it as textual.
try:
- u_str = file_contents.decode('utf-8', 'strict')
+ u_str = six.ensure_text(file_contents)
is_long = len(u_str.split('\n')) > SOURCE_FILE_MAX_LINES
return u_str, False, is_long
except UnicodeDecodeError:
@@ -168,7 +168,7 @@
# Fall back on latin-1. This will always succeed, since every byte maps to
# something in latin-1, even if that something is gibberish.
- u_str = file_contents.decode('latin-1', 'strict')
+ u_str = six.ensure_text(file_contents, encoding='latin-1')
lines = u_str.split('\n')
is_long = len(lines) > SOURCE_FILE_MAX_LINES
diff --git a/framework/flaskservlet.py b/framework/flaskservlet.py
deleted file mode 100644
index bc543d8..0000000
--- a/framework/flaskservlet.py
+++ /dev/null
@@ -1,881 +0,0 @@
-# Copyright 2022 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
-"""Base classes for Monorail Flask servlets.
-
-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:
- FlaskServlet: abstract base class for all Monorail flask servlets.
-"""
-
-import gc
-import os
-import httplib
-import logging
-import time
-from businesslogic import work_env
-
-import ezt
-from features import features_bizobj, hotlist_views
-import flask
-import httpagentparser
-from project import project_constants
-from proto 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 tracker import tracker_views
-
-NONCE_LENGTH = 32
-
-if not settings.unit_test_mode:
- import MySQLdb
-
-class FlaskServlet(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 overridden in subclasses to be one of these:
-
- MAIN_TAB_ISSUES = 't2'
- MAIN_TAB_PEOPLE = 't3'
- IN_TAB_PEOPLE = 't3'
- MAIN_TAB_PROCESS = 't4'
- MAIN_TAB_UPDATES = 't5'
- MAIN_TAB_ADMIN = 't6'
- PROCESS_TAB_SUMMARY = 'st1'
- PROCESS_TAB_STATUSES = 'st3'
- PROCESS_TAB_LABELS = 'st4'
- PROCESS_TAB_RULES = 'st5'
- PROCESS_TAB_TEMPLATES = 'st6'
- PROCESS_TAB_COMPONENTS = 'st7'
- PROCESS_TAB_VIEWS = 'st8'
- ADMIN_TAB_META = 'st1'
- ADMIN_TAB_ADVANCED = 'st9'
- HOTLIST_TAB_ISSUES = 'ht2'
- HOTLIST_TAB_PEOPLE = 'ht3'
- HOTLIST_TAB_DETAILS = 'ht4'
-
- # Most forms require a security token, however if a form is really
- # just redirecting to a search GET request without writing any data,
- # subclass can override this to allow anonymous use.
- CHECK_SECURITY_TOKEN = True
-
- # Some pages might be posted to by clients outside of Monorail.
- # ie: The issue entry page, by the issue filing wizard. In these cases,
- # we can allow an xhr-scoped XSRF token to be used to post to the page.
- ALLOW_XHR = False
-
- # This value should not typically be overridden.
- _TEMPLATE_PATH = framework_constants.TEMPLATE_PATH
-
- _PAGE_TEMPLATE = None # Normally overridden in subclasses.
- _ELIMINATE_BLANK_LINES = False
-
- _MISSING_PERMISSIONS_TEMPLATE = 'sitewide/403-page.ezt'
-
- def __init__(self, services=None, content_type='text/html; charset=UTF-8'):
- """Load and parse the template, saving it for later use."""
- if self._PAGE_TEMPLATE: # specified in subclasses
- template_path = self._TEMPLATE_PATH + self._PAGE_TEMPLATE
- self.template = template_helpers.GetTemplate(
- template_path, eliminate_blank_lines=self._ELIMINATE_BLANK_LINES)
- else:
- self.template = None
-
- self._missing_permissions_template = template_helpers.MonorailTemplate(
- self._TEMPLATE_PATH + self._MISSING_PERMISSIONS_TEMPLATE)
- self.services = services or flask.current_app.config['services']
- self.content_type = content_type
- self.mr = None
- self.request = flask.request
- self.request_path = None
- self.response = None
- self.ratelimiter = ratelimiter.RateLimiter()
- self.redirect_url = None
-
- # 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)
-
- #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.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'))
-
- if self.services.cache_manager:
- try:
- with self.mr.profiler.Phase('distributed invalidation'):
- self.services.cache_manager.DoDistributedInvalidation(self.mr.cnxn)
-
- except MySQLdb.OperationalError as e:
- logging.exception(e)
- page_data = {
- 'http_response_code': httplib.SERVICE_UNAVAILABLE,
- 'requested_url': self.request.url,
- }
- self.template = template_helpers.GetTemplate(
- 'templates/framework/database-maintenance.ezt',
- eliminate_blank_lines=self._ELIMINATE_BLANK_LINES)
- self.template.WriteFlaskResponse(
- self.response, page_data, content_type='text/html')
- return self.response
-
- try:
- self.ratelimiter.CheckStart(self.request)
-
- with self.mr.profiler.Phase('parsing request and doing lookups'):
- self.mr.ParseFlaskRequest(self.request, self.services)
-
- self.response.headers['X-Frame-Options'] = 'SAMEORIGIN'
-
- if self.request.method == 'POST':
- self.post()
- if self.redirect_url:
- return self.redirect(self.redirect_url)
- elif self.request.method == 'GET':
- self.get()
- except exceptions.NoSuchUserException as e:
- logging.info('Trapped NoSuchUserException %s', e)
- flask.abort(404, 'user not found')
-
- except exceptions.NoSuchGroupException as e:
- logging.warning('Trapped NoSuchGroupException %s', e)
- flask.abort(404, 'user group not found')
-
- except exceptions.InputException as e:
- logging.info('Rejecting invalid input: %r', e)
- self.response.status_code = httplib.BAD_REQUEST
-
- except exceptions.NoSuchProjectException as e:
- logging.info('Rejecting invalid request: %r', e)
- self.response.status_code = httplib.NOT_FOUND
-
- except xsrf.TokenIncorrect as e:
- logging.info('Bad XSRF token: %r', e.message)
- self.response.status_code = httplib.BAD_REQUEST
-
- except permissions.BannedUserException as e:
- logging.warning('The user has been banned')
- url = framework_helpers.FormatAbsoluteURL(
- self.mr, urls.BANNED, include_project=False, copy_params=False)
- self.redirect(url, abort=True)
-
- except ratelimiter.RateLimitExceeded as e:
- logging.info('RateLimitExceeded Exception %s', e)
- self.response.status_code = httplib.BAD_REQUEST
- self.response.set_data('Slow your roll.')
-
- finally:
- self.mr.CleanUp()
- self.ratelimiter.CheckEnd(self.request, time.time(), handler_start_time)
-
- total_processing_time = time.time() - handler_start_time
- logging.info(
- 'Processed request in %d ms', int(total_processing_time * 1000))
-
- end_count0, end_count1, end_count2 = gc.get_count()
- logging.info('gc counts: %d %d %d', end_count0, end_count1, end_count2)
- # 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()
-
- return self.response
-
- def get(self):
- """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 ignore them for now.
- """
- page_data = {}
- nonce = framework_helpers.MakeRandomKey(length=NONCE_LENGTH)
- try:
- csp_header = 'Content-Security-Policy'
- csp_scheme = 'https:'
- if settings.local_mode:
- csp_header = 'Content-Security-Policy-Report-Only'
- csp_scheme = 'http:'
- 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'):
- 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'])
- csp_supports_report_sample = (
- (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,
- }))
-
- # add the function to get data and render page
- page_data.update(self._GatherFlagData(self.mr))
-
- # Page-specific work happens in this call.
- page_data.update(self._DoPageProcessing(self.mr, nonce))
-
- self._AddHelpDebugPageData(page_data)
-
- with self.mr.profiler.Phase('rendering template'):
- self._RenderResponse(page_data)
-
- except (servlet_helpers.MethodNotSupportedError, NotImplementedError) as e:
- # Instead of these pages throwing 500s display the 404 message and log.
- # The motivation of this is to minimize 500s on the site to keep alerts
- # meaningful during fuzzing. For more context see
- # https://bugs.chromium.org/p/monorail/issues/detail?id=659
- logging.warning('Trapped NotImplementedError %s', e)
- 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'
- 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)
- logging.warning('mr.auth.effective_ids is %s', self.mr.auth.effective_ids)
- 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)
- else:
- # Display the missing permissions template.
- page_data = {
- 'reason': e.message,
- 'http_response_code': httplib.FORBIDDEN,
- }
- with self.mr.profiler.Phase('gather base data'):
- page_data.update(self.GatherBaseData(self.mr, nonce))
- self._AddHelpDebugPageData(page_data)
- self._missing_permissions_template.WriteFlaskResponse(
- self.response, page_data, content_type=self.content_type)
-
- 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)
- self.response.status_code = httplib.BAD_REQUEST
-
- def _RenderResponse(self, page_data):
- logging.info('rendering response len(page_data) is %r', len(page_data))
- self.template.WriteFlaskResponse(
- 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."""
- with mr.profiler.Phase('basic processing'):
- 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'):
- page_data.update(self.GatherPageData(mr))
- page_data.update(mr.form_overrides)
- template_helpers.ExpandLabels(page_data)
- self._RecordVisitTime(mr)
-
- return page_data
-
- def _DoFormProcessing(self, request, mr):
- """Do user lookups and handle form data."""
- self._DoCommonRequestProcessing(request, mr)
-
- if self.CHECK_SECURITY_TOKEN:
- try:
- xsrf.ValidateToken(
- request.values.get('token'), mr.auth.user_id, self.request_path)
- except xsrf.TokenIncorrect as err:
- if self.ALLOW_XHR:
- xsrf.ValidateToken(
- request.values.get('token'), mr.auth.user_id, 'xhr')
- else:
- raise err
-
- self.redirect_url = self.ProcessFormData(mr, request.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 highlighted.
- if self.redirect_url:
- return self.redirect(self.redirect_url, abort=True)
- else:
- assert self.response.response
-
- 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 _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'
-
- # 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."""
- project = mr.project
-
- project_summary = ''
- project_alert = None
- project_read_only = False
- project_home_page = ''
- project_thumbnail_url = ''
- if project:
- project_summary = project.summary
- project_alert = servlet_helpers.CalcProjectAlert(project)
- project_read_only = project.read_only_reason
- project_home_page = project.home_page
- project_thumbnail_url = tracker_views.LogoView(project).thumbnail_url
-
- with work_env.WorkEnv(mr, self.services) as we:
- is_project_starred = False
- project_view = None
- if mr.project:
- if permissions.UserCanViewProject(mr.auth.user_pb,
- mr.auth.effective_ids, mr.project):
- is_project_starred = we.IsProjectStarred(mr.project_id)
- project_view = template_helpers.PBProxy(mr.project)
-
- grid_x_attr = None
- grid_y_attr = None
- hotlist_view = None
- if mr.hotlist:
- users_by_id = framework_views.MakeAllUserViews(
- mr.cnxn, self.services.user,
- 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))
- grid_x_attr = mr.x.lower()
- grid_y_attr = mr.y.lower()
-
- app_version = os.environ.get('CURRENT_VERSION_ID')
-
- viewed_username = None
- if mr.viewed_user_auth.user_view:
- viewed_username = mr.viewed_user_auth.user_view.username
-
- config = None
- if mr.project_id and self.services.config:
- with mr.profiler.Phase('getting config'):
- config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
- grid_x_attr = (mr.x or config.default_x_attr).lower()
- grid_y_attr = (mr.y or config.default_y_attr).lower()
-
- viewing_self = mr.auth.user_id == mr.viewed_user_auth.user_id
- offer_saved_queries_subtab = (
- viewing_self or mr.auth.user_pb and mr.auth.user_pb.is_site_admin)
-
- login_url = servlet_helpers.SafeCreateLoginURL(mr)
- logout_url = servlet_helpers.SafeCreateLogoutURL(mr)
- logout_url_goto_home = users.create_logout_url('/')
- version_base = servlet_helpers.VersionBaseURL(mr.request)
-
- base_data = {
- # EZT does not have constants for True and False, so we pass them in.
- 'True':
- ezt.boolean(True),
- 'False':
- ezt.boolean(False),
- 'local_mode':
- ezt.boolean(settings.local_mode),
- 'site_name':
- settings.site_name,
- 'show_search_metadata':
- ezt.boolean(False),
- 'page_template':
- self._PAGE_TEMPLATE,
- 'main_tab_mode':
- self._MAIN_TAB_MODE,
- 'project_summary':
- project_summary,
- 'project_home_page':
- project_home_page,
- 'project_thumbnail_url':
- project_thumbnail_url,
- 'hotlist_id':
- mr.hotlist_id,
- 'hotlist':
- hotlist_view,
- 'hostport':
- mr.request.host,
- 'absolute_base_url':
- '%s://%s' % (mr.request.scheme, mr.request.host),
- 'project_home_url':
- None,
- 'link_rel_canonical':
- None, # For specifying <link rel="canonical">
- 'projectname':
- mr.project_name,
- 'project':
- project_view,
- 'project_is_restricted':
- ezt.boolean(servlet_helpers.ProjectIsRestricted(mr)),
- 'offer_contributor_list':
- ezt.boolean(permissions.CanViewContributorList(mr, mr.project)),
- 'logged_in_user':
- mr.auth.user_view,
- 'form_token':
- None, # Set to a value below iff the user is logged in.
- 'form_token_path':
- None,
- 'token_expires_sec':
- None,
- 'xhr_token':
- None, # Set to a value below iff the user is logged in.
- 'flag_spam_token':
- None,
- 'nonce':
- nonce,
- 'perms':
- mr.perms,
- 'warnings':
- mr.warnings,
- 'errors':
- mr.errors,
- 'viewed_username':
- viewed_username,
- 'viewed_user':
- mr.viewed_user_auth.user_view,
- 'viewed_user_pb':
- template_helpers.PBProxy(mr.viewed_user_auth.user_pb),
- 'viewing_self':
- ezt.boolean(viewing_self),
- 'viewed_user_id':
- mr.viewed_user_auth.user_id,
- 'offer_saved_queries_subtab':
- ezt.boolean(offer_saved_queries_subtab),
- 'currentPageURL':
- mr.current_page_url,
- 'currentPageURLEncoded':
- mr.current_page_url_encoded,
- 'login_url':
- login_url,
- 'logout_url':
- logout_url,
- 'logout_url_goto_home':
- logout_url_goto_home,
- 'continue_issue_id':
- mr.continue_issue_id,
- 'feedback_email':
- settings.feedback_email,
- 'category_css':
- None, # Used to specify a category of stylesheet
- 'category2_css':
- None, # specify a 2nd category of stylesheet if needed.
- 'page_css':
- None, # Used to add a stylesheet to a specific page.
- 'can':
- mr.can,
- 'query':
- mr.query,
- 'colspec':
- None,
- 'sortspec':
- mr.sort_spec,
-
- # Options for issuelist display
- 'grid_x_attr':
- grid_x_attr,
- 'grid_y_attr':
- grid_y_attr,
- 'grid_cell_mode':
- mr.cells,
- 'grid_mode':
- None,
- 'list_mode':
- None,
- 'chart_mode':
- None,
- 'is_cross_project':
- ezt.boolean(False),
-
- # for project search (some also used in issue search)
- 'start':
- mr.start,
- 'num':
- mr.num,
- 'groupby':
- mr.group_by_spec,
- 'q_field_size':
- (
- min(
- framework_constants.MAX_ARTIFACT_SEARCH_FIELD_SIZE,
- max(
- framework_constants.MIN_ARTIFACT_SEARCH_FIELD_SIZE,
- len(mr.query) + framework_constants.AUTOSIZE_STEP))),
- 'mode':
- None, # Display mode, e.g., grid mode.
- 'ajah':
- mr.ajah,
- 'table_title':
- mr.table_title,
- 'alerts':
- alerts.AlertsView(mr), # For alert.ezt
- 'project_alert':
- project_alert,
- 'title':
- None, # First part of page title
- 'title_summary':
- None, # Appended to title on artifact detail pages
- 'project_read_only':
- ezt.boolean(project_read_only),
- 'site_read_only':
- ezt.boolean(settings.read_only),
- 'banner_time':
- servlet_helpers.GetBannerTime(settings.banner_time),
- 'read_only':
- ezt.boolean(settings.read_only or project_read_only),
- 'site_banner_message':
- settings.banner_message,
- 'robots_no_index':
- None,
- 'analytics_id':
- settings.analytics_id,
- 'is_project_starred':
- ezt.boolean(is_project_starred),
- 'version_base':
- version_base,
- 'app_version':
- app_version,
- 'gapi_client_id':
- settings.gapi_client_id,
- 'viewing_user_page':
- ezt.boolean(False),
- 'old_ui_url':
- None,
- 'new_ui_url':
- None,
- 'is_member':
- ezt.boolean(False),
- }
-
- if mr.project:
- base_data['project_home_url'] = '/p/%s' % mr.project_name
-
- # Always add xhr-xsrf token because even anon users need some
- # pRPC methods, e.g., autocomplete, flipper, and charts.
- base_data['token_expires_sec'] = xsrf.TokenExpiresSec()
- base_data['xhr_token'] = xsrf.GenerateToken(
- 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)
- base_data['form_token'] = xsrf.GenerateToken(
- mr.auth.user_id, form_token_path)
- base_data['form_token_path'] = form_token_path
-
- return base_data
-
- 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.
-
- Returns:
- A dict of values to drive on-page user help, to be added to page_data.
- """
- 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']
- if (mr.auth.user_pb.vacation_message and
- 'you_are_on_vacation' not in dismissed):
- help_data['cue'] = 'you_are_on_vacation'
- if (mr.auth.user_pb.email_bounce_timestamp and
- 'your_email_bounced' not in dismissed):
- help_data['cue'] = 'your_email_bounced'
- if mr.auth.user_pb.linked_parent_id:
- # This one is not dismissable.
- help_data['account_cue'] = 'switch_to_parent_account'
- parent_email = self.services.user.LookupUserEmail(
- mr.cnxn, mr.auth.user_pb.linked_parent_id)
- help_data['parent_email'] = parent_email
-
- return help_data
-
- def GatherDebugData(self, mr, page_data):
- """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'
- else:
- debug_url = mr.current_page_url + '?debug=1'
-
- return {
- '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."""
- mr.PrepareForReentry(echo_data)
- self.get()
-
- def _RecordVisitTime(self, mr, now=None):
- """Record the signed in user's last visit time, if possible."""
- now = now or int(time.time())
- if not settings.read_only and mr.auth.user_id:
- user_pb = mr.auth.user_pb
- if (user_pb.last_visit_timestamp <
- 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)
diff --git a/framework/framework_bizobj.py b/framework/framework_bizobj.py
index bacaec5..77a70ac 100644
--- a/framework/framework_bizobj.py
+++ b/framework/framework_bizobj.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Business objects for Monorail's framework.
@@ -22,7 +21,7 @@
import settings
from framework import exceptions
from framework import framework_constants
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from services import client_config_svc
@@ -30,7 +29,7 @@
RE_COLUMN_NAME = r'\w+[\w+-.]*\w+'
# Compiled regexp to match a valid column specification.
-RE_COLUMN_SPEC = re.compile('(%s(\s%s)*)*$' % (RE_COLUMN_NAME, RE_COLUMN_NAME))
+RE_COLUMN_SPEC = re.compile(r'(%s(\s%s)*)*$' % (RE_COLUMN_NAME, RE_COLUMN_NAME))
def WhichUsersShareAProject(cnxn, services, user_effective_ids, other_users):
diff --git a/framework/framework_constants.py b/framework/framework_constants.py
index 1490135..7609a96 100644
--- a/framework/framework_constants.py
+++ b/framework/framework_constants.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Some constants used throughout Monorail."""
from __future__ import print_function
diff --git a/framework/framework_helpers.py b/framework/framework_helpers.py
index 47c34ef..cb13931 100644
--- a/framework/framework_helpers.py
+++ b/framework/framework_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Helper functions and classes used throughout Monorail."""
@@ -30,7 +29,7 @@
from framework import template_helpers
from framework import timestr
from framework import urls
-from proto import user_pb2
+from mrproto import user_pb2
from services import client_config_svc
# AttachmentUpload holds the information of an incoming uploaded
diff --git a/framework/framework_views.py b/framework/framework_views.py
index 17dead8..ca91a56 100644
--- a/framework/framework_views.py
+++ b/framework/framework_views.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""View classes to make it easy to display framework objects in EZT."""
from __future__ import print_function
@@ -19,7 +18,7 @@
from framework import permissions
from framework import template_helpers
from framework import timestr
-from proto import user_pb2
+from mrproto import user_pb2
from services import client_config_svc
import settings
diff --git a/framework/gcs_helpers.py b/framework/gcs_helpers.py
index 9da5b11..fd282e0 100644
--- a/framework/gcs_helpers.py
+++ b/framework/gcs_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Set of helpers for interacting with Google Cloud Storage."""
from __future__ import print_function
@@ -10,6 +9,7 @@
import logging
import os
+import six
from six.moves import urllib
import uuid
@@ -96,7 +96,7 @@
# Don't log the whole exception because we don't need to see
# this on the Cloud Error Reporting page.
logging.info('Got LargeImageError on image with %d bytes', len(content))
- except Exception, e:
+ except Exception as e:
# Do not raise exception for incorrectly formed images.
# See https://bugs.chromium.org/p/monorail/issues/detail?id=597 for more
# detail.
@@ -132,7 +132,7 @@
"""Request that devstorage API signs a GCS content URL."""
resp = urlfetch.fetch(url, follow_redirects=False)
redir = resp.headers["Location"]
- return redir
+ return six.ensure_str(redir)
def SignUrl(bucket, object_id):
@@ -149,7 +149,7 @@
cache_key = 'gcs-object-url-%s' % object_id
cached = memcache.get(key=cache_key)
if cached is not None:
- return cached
+ return six.ensure_str(cached)
if IS_DEV_APPSERVER:
attachment_url = '/_ah/gcs/%s%s' % (bucket, object_id)
diff --git a/framework/grid_view_helpers.py b/framework/grid_view_helpers.py
index 44af6b7..2ed4e70 100644
--- a/framework/grid_view_helpers.py
+++ b/framework/grid_view_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes and functions for displaying grids of project artifacts.
@@ -24,7 +23,7 @@
from framework import table_view_helpers
from framework import template_helpers
from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from tracker import tracker_bizobj
from tracker import tracker_constants
from tracker import tracker_helpers
@@ -87,7 +86,7 @@
return sorting.MAX_STRING # Undefined values sort last.
try:
# well-known values sort by index
- return well_known_list.index(value)
+ return '%09d' % well_known_list.index(value)
except ValueError:
return value # odd-ball values lexicographically after all well-known ones
diff --git a/framework/jsonfeed.py b/framework/jsonfeed.py
index 1eff87d..b357fd8 100644
--- a/framework/jsonfeed.py
+++ b/framework/jsonfeed.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""This file defines a subclass of Servlet for JSON feeds.
@@ -21,10 +20,9 @@
import settings
from framework import framework_constants
-from framework import flaskservlet
-from framework import servlet_helpers
from framework import permissions
from framework import servlet
+from framework import servlet_helpers
from framework import xsrf
from search import query2ast
@@ -56,110 +54,6 @@
def _DoRequestHandling(self, request, mr):
"""Do permission checking, page processing, and response formatting."""
try:
- # TODO(jrobbins): check the XSRF token even for anon users
- # after the next deployment.
- if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
- # Validate the XSRF token with the specific request path for this
- # servlet. But, not every XHR request has a distinct token, so just
- # use 'xhr' for ones that don't.
- # TODO(jrobbins): make specific tokens for:
- # user and project stars, issue options, check names.
- try:
- logging.info('request in jsonfeed is %r', request)
- xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path)
- except xsrf.TokenIncorrect:
- logging.info('using token path "xhr"')
- xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
-
- if self.CHECK_SAME_APP and not settings.local_mode:
- calling_app_id = request.headers.get('X-Appengine-Inbound-Appid')
- if calling_app_id != app_identity.get_application_id():
- self.response.status = http_client.FORBIDDEN
- return
-
- self._CheckForMovedProject(mr, request)
- self.AssertBasePermission(mr)
-
- json_data = self.HandleRequest(mr)
-
- self._RenderJsonResponse(json_data)
-
- 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)
- except permissions.PermissionException as e:
- logging.info('Trapped PermissionException %s', e)
- self.response.status = http_client.FORBIDDEN
-
- # pylint: disable=unused-argument
- # pylint: disable=arguments-differ
- # Note: unused arguments necessary because they are specified in
- # registerpages.py as an extra URL validation step even though we
- # do our own URL parsing in monorailrequest.py
- def get(self, project_name=None, viewed_username=None, hotlist_id=None):
- """Collect page-specific and generic info, then render the page.
-
- Args:
- project_name: string project name parsed from the URL by webapp2,
- but we also parse it out in our code.
- viewed_username: string user email parsed from the URL by webapp2,
- but we also parse it out in our code.
- hotlist_id: string hotlist id parsed from the URL by webapp2,
- but we also parse it out in our code.
- """
- self._DoRequestHandling(self.mr.request, self.mr)
-
- # pylint: disable=unused-argument
- # pylint: disable=arguments-differ
- def post(self, project_name=None, viewed_username=None, hotlist_id=None):
- """Parse the request, check base perms, and call form-specific code."""
- self._DoRequestHandling(self.mr.request, self.mr)
-
- def _RenderJsonResponse(self, json_data):
- """Serialize the data as JSON so that it can be sent to the browser."""
- json_str = json.dumps(json_data, indent=self.JSON_INDENT)
- logging.debug(
- 'Sending JSON response: %r length: %r',
- json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str))
- self.response.content_type = framework_constants.CONTENT_TYPE_JSON
- self.response.headers['X-Content-Type-Options'] = (
- framework_constants.CONTENT_TYPE_JSON_OPTIONS)
- self.response.write(XSSI_PREFIX)
- self.response.write(json_str)
-
-
-class InternalTask(JsonFeed):
- """Internal tasks are JSON feeds that can only be reached by our own code."""
-
- CHECK_SECURITY_TOKEN = False
-
-
-class FlaskJsonFeed(flaskservlet.FlaskServlet):
- """A convenient base class for JSON feeds."""
-
- # By default, JSON output is compact. Subclasses can set this to
- # an integer, like 4, for pretty-printed output.
- JSON_INDENT = None
-
- # Some JSON handlers can only be accessed from our own app.
- CHECK_SAME_APP = False
-
- def HandleRequest(self, _mr):
- """Override this method to implement handling of the request.
-
- Args:
- mr: common information parsed from the HTTP request.
-
- Returns:
- A dictionary of json data.
- """
- raise servlet_helpers.MethodNotSupportedError()
-
- def _DoRequestHandling(self, request, mr):
- """Do permission checking, page processing, and response formatting."""
- try:
if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
try:
logging.info('request in jsonfeed is %r', request)
@@ -184,7 +78,7 @@
except query2ast.InvalidQueryError as e:
logging.warning('Trapped InvalidQueryError: %s', e)
logging.exception(e)
- msg = e.message if e.message else 'invalid query'
+ msg = str(e) if str(e) else 'invalid query'
self.abort(400, msg)
except permissions.PermissionException as e:
logging.info('Trapped PermissionException %s', e)
@@ -199,11 +93,11 @@
"""Collect page-specific and generic info, then render the page.
Args:
- project_name: string project name parsed from the URL by webapp2,
+ project_name: string project name parsed from the URL by Flask,
but we also parse it out in our code.
- viewed_username: string user email parsed from the URL by webapp2,
+ viewed_username: string user email parsed from the URL by Flask,
but we also parse it out in our code.
- hotlist_id: string hotlist id parsed from the URL by webapp2,
+ hotlist_id: string hotlist id parsed from the URL by Flask,
but we also parse it out in our code.
"""
self._DoRequestHandling(self.mr.request, self.mr)
@@ -226,7 +120,7 @@
self.response.set_data(XSSI_PREFIX + json_str)
-class FlaskInternalTask(FlaskJsonFeed):
+class InternalTask(JsonFeed):
"""Internal tasks are JSON feeds that can only be reached by our own code."""
CHECK_SECURITY_TOKEN = False
diff --git a/framework/logger.py b/framework/logger.py
index d2a8a0d..2a4da8e 100644
--- a/framework/logger.py
+++ b/framework/logger.py
@@ -1,4 +1,4 @@
-# Copyright 2022 The Chromium Authors. All rights reserved.
+# 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.
""""Helper methods for structured logging."""
diff --git a/framework/monitoring.py b/framework/monitoring.py
index 08a1e23..c7062f9 100644
--- a/framework/monitoring.py
+++ b/framework/monitoring.py
@@ -1,6 +1,6 @@
-# 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.
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Monitoring ts_mon custom to monorail."""
diff --git a/framework/monorailcontext.py b/framework/monorailcontext.py
index 76ecff4..45e4637 100644
--- a/framework/monorailcontext.py
+++ b/framework/monorailcontext.py
@@ -1,7 +1,6 @@
-# Copyright 2018 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 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Context object to hold utility objects used during request processing.
"""
diff --git a/framework/monorailrequest.py b/framework/monorailrequest.py
index 7fe2918..52f3e52 100644
--- a/framework/monorailrequest.py
+++ b/framework/monorailrequest.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes to hold information parsed from a request.
@@ -19,13 +18,12 @@
from six.moves import urllib
import ezt
+import flask
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
@@ -39,12 +37,12 @@
from framework import profiler
from framework import sql
from framework import template_helpers
-from proto import api_pb2_v1
+from mrproto import api_pb2_v1
from tracker import tracker_bizobj
from tracker import tracker_constants
-_HOSTPORT_RE = re.compile('^[-a-z0-9.]+(:\d+)?$', re.I)
+_HOSTPORT_RE = re.compile(r'^[-a-z0-9.]+(:\d+)?$', re.I)
# TODO(jrobbins): Stop extending MonorailContext and change whole servlet
@@ -231,63 +229,6 @@
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.request_path = request.path
- self.current_page_url = request.url
- self.current_page_url_encoded = urllib.parse.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)
-
- 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 ParseFlaskRequest(self, request, services, do_user_lookups=True):
"""Parse tons of useful info from the given flask request object.
Args:
@@ -300,7 +241,7 @@
self.request_path = request.base_url[len(request.host_url) - 1:]
self.current_page_url = request.url
self.current_page_url_encoded = urllib.parse.quote_plus(
- self.current_page_url)
+ six.ensure_str(self.current_page_url))
# Only accept a hostport from the request that looks valid.
if not _HOSTPORT_RE.match(request.host):
@@ -502,10 +443,10 @@
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')
+ flask.abort(404, 'user not found')
if not self.viewed_user_auth.user_id:
- webapp2.abort(404, 'user not found')
+ flask.abort(404, 'user not found')
def _LookupProject(self, services):
"""Get information about the current project (if any) from the request.
@@ -530,7 +471,7 @@
self.hotlist_id = hotlist_id_dict[(
self.hotlist_name, self.viewed_user_auth.user_id)]
except KeyError:
- webapp2.abort(404, 'invalid hotlist')
+ flask.abort(404, 'invalid hotlist')
if not self.hotlist_id:
logging.info('no hotlist_id or bad hotlist_name, so no hotlist')
@@ -540,7 +481,7 @@
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')
+ flask.abort(404, 'invalid hotlist')
def _LookupLoggedInUser(self, services):
"""Get information about the signed-in user (if any) from the request."""
@@ -601,7 +542,7 @@
value = self.request.params.get(query_param_name)
else:
value = self.request.values.get(query_param_name)
- assert value is None or isinstance(value, six.text_type)
+ assert value is None or isinstance(value, six.string_types)
using_default = value is None
if using_default:
value = default_value
@@ -635,8 +576,10 @@
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)
+ value = self.GetIntParam(query_param_name, default_value=default_value)
+ if value is None:
+ return 0
+ return max(value, 0)
def GetListParam(self, query_param_name, default_value=None):
"""Get a list of strings from the URL or default."""
@@ -742,7 +685,7 @@
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')
+ flask.abort(404, 'user not found')
except ValueError:
viewed_email = viewed_user_val
diff --git a/framework/paginate.py b/framework/paginate.py
index bbe0998..817475f 100644
--- a/framework/paginate.py
+++ b/framework/paginate.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes that help display pagination widgets for result sets."""
from __future__ import print_function
@@ -9,8 +8,10 @@
from __future__ import absolute_import
import base64
-import logging
+import binascii
+import hashlib
import hmac
+import six
import ezt
from google.protobuf import message
@@ -19,7 +20,7 @@
from framework import exceptions
from framework import framework_helpers
from services import secrets_svc
-from proto import secrets_pb2
+from mrproto import secrets_pb2
def GeneratePageToken(request_contents, start):
@@ -34,12 +35,12 @@
Returns:
String next_page_token that is a serialized PageTokenContents object.
"""
- digester = hmac.new(secrets_svc.GetPaginationKey())
- digester.update(request_contents.SerializeToString())
+ digester = hmac.new(secrets_svc.GetPaginationKey(), digestmod=hashlib.md5)
+ digester.update(six.ensure_binary(request_contents.SerializeToString()))
token_contents = secrets_pb2.PageTokenContents(
start=start,
encrypted_list_request_contents=digester.digest())
- serialized_token = token_contents.SerializeToString()
+ serialized_token = six.ensure_binary(token_contents.SerializeToString())
# Page tokens must be URL-safe strings (see aip.dev/158)
# and proto string fields must be utf-8 strings while
# `SerializeToString()` returns binary bytes contained in a str type.
@@ -48,7 +49,7 @@
def ValidateAndParsePageToken(token, request_contents):
- # type: (str, secrets_pb2.ListRequestContents) -> int
+ # type: (bytes, secrets_pb2.ListRequestContents) -> int
"""Returns the start index of the page if the token is valid.
Args:
@@ -67,7 +68,7 @@
try:
decoded_serialized_token = base64.b64decode(token)
token_contents.ParseFromString(decoded_serialized_token)
- except (message.DecodeError, TypeError):
+ except (message.DecodeError, TypeError, binascii.Error): # TypeError in Py2.
raise exceptions.PageTokenException('Invalid page token.')
start = token_contents.start
diff --git a/framework/pbproxy_test_pb2.py b/framework/pbproxy_test_pb2.py
index 3c47ae1..16db03d 100644
--- a/framework/pbproxy_test_pb2.py
+++ b/framework/pbproxy_test_pb2.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Message classes for use by template_helpers_test."""
from __future__ import print_function
diff --git a/framework/permissions.py b/framework/permissions.py
index ac46af6..37f6cdc 100644
--- a/framework/permissions.py
+++ b/framework/permissions.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes and functions to implement permission checking.
@@ -32,10 +31,10 @@
import settings
from framework import framework_bizobj
from framework import framework_constants
-from proto import project_pb2
-from proto import site_pb2
-from proto import tracker_pb2
-from proto import usergroup_pb2
+from mrproto import project_pb2
+from mrproto import site_pb2
+from mrproto import tracker_pb2
+from mrproto import usergroup_pb2
from tracker import tracker_bizobj
# Constants that define permissions.
@@ -333,40 +332,53 @@
# Project owners can view and edit artifacts in a LIVE project.
(OWNER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS):
- OWNER_ACTIVE_PERMISSIONSET,
+ OWNER_ACTIVE_PERMISSIONSET,
# Project owners can view, but not edit artifacts in ARCHIVED.
# Note: EDIT_PROJECT is not enough permission to change an ARCHIVED project
# back to LIVE if a delete_time was set.
(OWNER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS):
- OWNER_INACTIVE_PERMISSIONSET,
+ OWNER_INACTIVE_PERMISSIONSET,
# Project members can view their own project, regardless of state.
(COMMITTER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS):
- COMMITTER_ACTIVE_PERMISSIONSET,
+ COMMITTER_ACTIVE_PERMISSIONSET,
(COMMITTER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS):
- COMMITTER_INACTIVE_PERMISSIONSET,
+ COMMITTER_INACTIVE_PERMISSIONSET,
# Project contributors can view their own project, regardless of state.
(CONTRIBUTOR_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS):
- CONTRIBUTOR_ACTIVE_PERMISSIONSET,
+ CONTRIBUTOR_ACTIVE_PERMISSIONSET,
(CONTRIBUTOR_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS):
- CONTRIBUTOR_INACTIVE_PERMISSIONSET,
+ CONTRIBUTOR_INACTIVE_PERMISSIONSET,
- # Non-members users can read and comment in projects with access == ANYONE
- (USER_ROLE, project_pb2.ProjectState.LIVE,
- project_pb2.ProjectAccess.ANYONE):
- USER_PERMISSIONSET,
+ # Non-members users can read and comment in projects with access == ANYONE.
+ (
+ USER_ROLE, project_pb2.ProjectState.LIVE,
+ project_pb2.ProjectAccess.ANYONE):
+ USER_PERMISSIONSET,
- # Anonymous users can only read projects with access == ANYONE.
- (ANON_ROLE, project_pb2.ProjectState.LIVE,
- project_pb2.ProjectAccess.ANYONE):
- READ_ONLY_PERMISSIONSET,
+ # Non-members users can read archived projects with access == ANYONE.
+ (
+ USER_ROLE, project_pb2.ProjectState.ARCHIVED,
+ project_pb2.ProjectAccess.ANYONE):
+ READ_ONLY_PERMISSIONSET,
+
+ # Anonymous users can only read projects with access == ANYONE,
+ # regardless of state.
+ (
+ ANON_ROLE, project_pb2.ProjectState.LIVE,
+ project_pb2.ProjectAccess.ANYONE):
+ READ_ONLY_PERMISSIONSET,
+ (
+ ANON_ROLE, project_pb2.ProjectState.ARCHIVED,
+ project_pb2.ProjectAccess.ANYONE):
+ READ_ONLY_PERMISSIONSET,
# Permissions for site pages, e.g., creating a new project
(USER_ROLE, UNDEFINED_STATUS, UNDEFINED_ACCESS):
- PermissionSet([CREATE_PROJECT, CREATE_GROUP, CREATE_HOTLIST]),
- }
+ PermissionSet([CREATE_PROJECT, CREATE_GROUP, CREATE_HOTLIST]),
+}
def GetPermissions(user, effective_ids, project):
"""Return a permission set appropriate for the user and project.
@@ -428,7 +440,7 @@
def UpdateIssuePermissions(
perms, project, issue, effective_ids, granted_perms=None, config=None):
- """Update the PermissionSet for an specific issue.
+ """Update the PermissionSet for a specific issue.
Take into account granted permissions and label restrictions to filter the
permissions, and updates the VIEW and EDIT_ISSUE permissions depending on the
@@ -488,7 +500,7 @@
filtered_perms.update(granted_perms)
# The VIEW perm might have been removed due to restrictions, but the issue
- # owner, reporter, cc and approvers can always be an issue.
+ # owner, reporter, cc and approvers can always view an issue.
allowed_ids = set(
tracker_bizobj.GetCcIds(issue)
+ tracker_bizobj.GetApproverIds(issue)
@@ -507,7 +519,8 @@
# The EDIT_ISSUE permission might have been removed due to restrictions, but
# the owner always has permission to edit it.
- if effective_ids and tracker_bizobj.GetOwnerId(issue) in effective_ids:
+ if (effective_ids and tracker_bizobj.GetOwnerId(issue) in effective_ids and
+ project and project.state != project_pb2.ProjectState.ARCHIVED):
filtered_perms.add(EDIT_ISSUE.lower())
return PermissionSet(filtered_perms, perms.consider_restrictions)
@@ -1113,6 +1126,36 @@
return perms.CanUsePerm(EDIT_ISSUE_APPROVAL, effective_ids, project, [])
+def CanEditProjectConfig(mr, services):
+ """ Special function to check if a user can edit a project config.
+
+ This function accounts for special edge cases pertaining only to project
+ configuration editing permissions, such as checking if a project is frozen
+ for config edits or if a user is in the allowlist of users who can override
+ a config freeze.
+
+ Args:
+ mr: MonorailRequest object.
+ services: reference to database layer.
+
+ Returns:
+ True if the user can edit the project.
+ """
+ if mr.project.project_id not in settings.config_freeze_project_ids:
+ return mr.perms.CanUsePerm(
+ EDIT_PROJECT, mr.auth.effective_ids, mr.project, [])
+
+ effective_users = services.user.GetUsersByIDs(
+ mr.cnxn, list(mr.auth.effective_ids))
+
+ for _, user in effective_users.items():
+ if user.email in settings.config_freeze_override_users.get(
+ mr.project.project_id, {}):
+ return True
+
+ return False
+
+
def CanViewComponentDef(effective_ids, perms, project, component_def):
"""Return True if a user can view the given component definition."""
if not effective_ids.isdisjoint(component_def.admin_ids):
@@ -1122,8 +1165,53 @@
return perms.CanUsePerm(VIEW, effective_ids, project, [])
-def CanEditComponentDef(effective_ids, perms, project, component_def, config):
- """Return True if a user can edit the given component definition."""
+def CanEditComponentDef(mr, services, component_def, config):
+ """ Checks if the currently logged in user can edit a component.
+
+ Args:
+ mr: MonorailRequest object.
+ services: reference to database layer.
+ component_def: the component to check permissions for.
+ config: project config of the project the component is in.
+
+ Returns:
+ True if a user can edit the given component definition."""
+ if mr.project.project_id in settings.config_freeze_project_ids:
+ return CanEditProjectConfig(mr, services)
+
+ if not mr.auth.effective_ids.isdisjoint(component_def.admin_ids):
+ return True # Component admins can edit that component.
+
+ # Check to see if user is admin of any parent component.
+ parent_components = tracker_bizobj.FindAncestorComponents(
+ config, component_def)
+ for parent in parent_components:
+ if not mr.auth.effective_ids.isdisjoint(parent.admin_ids):
+ return True
+
+ return CanEditProjectConfig(mr, services)
+
+
+def CanEditComponentDefLegacy(
+ effective_ids, perms, project, component_def, config):
+ """ Legacy version of CanEditComponentDef for codepaths without access to mr.
+ This function is entirely used in API clients.
+
+ Args:
+ effective_ids: Set containing IDs for the user and their groups
+ linked accounts, etc.
+ perms: PermissionSet for current user.
+ project: the project the component is in.
+ component_def: the component to check permissions for.
+ config: project config of the project the component is in.
+
+ Returns:
+ True if a user can edit the given component definition."""
+ # Do not bother checking if API client users are allowlisted to override
+ # the config freeze. Only human users are currently being allowlisted.
+ if project and project.project_id in settings.config_freeze_project_ids:
+ return False
+
if not effective_ids.isdisjoint(component_def.admin_ids):
return True # Component admins can edit that component.
diff --git a/framework/profiler.py b/framework/profiler.py
index 362585f..61e5092 100644
--- a/framework/profiler.py
+++ b/framework/profiler.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A simple profiler object to track how time is spent on a request.
@@ -156,14 +155,14 @@
self.ms = int(self.elapsed_seconds * 1000)
for sub in self.subphases:
if sub.elapsed_seconds is None:
- logging.warn('issue3182: subphase is %r', sub and sub.name)
+ logging.warning('issue3182: subphase is %r', sub and sub.name)
categorized = sum(sub.elapsed_seconds or 0.0 for sub in self.subphases)
self.uncategorized_ms = int((self.elapsed_seconds - categorized) * 1000)
return self.parent
def AccumulateStatLines(self, total_seconds, lines, indent=''):
# Only phases that took longer than 30ms are interesting.
- if self.ms <= 30:
+ if self.ms == 'in_progress' or self.ms <= 30:
return
percent = self.elapsed_seconds // total_seconds * 100
diff --git a/framework/ratelimiter.py b/framework/ratelimiter.py
index b2bbb25..4e1a0b8 100644
--- a/framework/ratelimiter.py
+++ b/framework/ratelimiter.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Request rate limiting implementation.
@@ -174,7 +173,7 @@
# monitor rate limit exceeded events with our standard tools.
# We return a 400 with a custom error message to the client,
# and this logging is so we can monitor it internally.
- logging.info('%s, %d' % (exception_obj.message, count))
+ logging.info('%s, %d' % (str(exception_obj), count))
self.limit_exceeded.increment()
diff --git a/framework/reap.py b/framework/reap.py
index 4654964..93f8b81 100644
--- a/framework/reap.py
+++ b/framework/reap.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A class to handle cron requests to expunge doomed and deletable projects."""
from __future__ import print_function
@@ -16,7 +15,7 @@
RUN_DURATION_LIMIT = 50 * 60 # 50 minutes
-class Reap(jsonfeed.FlaskInternalTask):
+class Reap(jsonfeed.InternalTask):
"""Look for doomed and deletable projects and delete them."""
def HandleRequest(self, mr):
diff --git a/framework/registerpages_helpers.py b/framework/registerpages_helpers.py
deleted file mode 100644
index 0073e57..0000000
--- a/framework/registerpages_helpers.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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
-
-"""This file sets up all the urls for monorail pages."""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-
-
-from six.moves import http_client
-import logging
-
-import webapp2
-
-
-def MakeRedirect(redirect_to_this_uri, permanent=True):
- """Return a new request handler class that redirects to the given URL."""
-
- class Redirect(webapp2.RequestHandler):
- """Redirect is a response handler that issues a redirect to another URI."""
-
- def get(self, **_kw):
- """Send the 301/302 response code and write the Location: redirect."""
- self.response.location = redirect_to_this_uri
- self.response.headers.add('Strict-Transport-Security',
- 'max-age=31536000; includeSubDomains')
- self.response.status = (
- http_client.MOVED_PERMANENTLY if permanent else http_client.FOUND)
-
- return Redirect
-
-
-def MakeRedirectInScope(uri_in_scope, scope, permanent=True, keep_qs=False):
- """Redirect to a URI within a given scope, e.g., per project or user.
-
- Args:
- uri_in_scope: a uri within a project or user starting with a slash.
- scope: a string indicating the uri-space scope:
- p for project pages
- u for user pages
- g for group pages
- permanent: True for a HTTP 301 permanently moved response code,
- otherwise a HTTP 302 temporarily moved response will be used.
- keep_qs: set to True to make the redirect retain the query string.
- When true, permanent is ignored.
-
- Example:
- self._SetupProjectPage(
- redirect.MakeRedirectInScope('/newpage', 'p'), '/oldpage')
-
- Returns:
- A class that can be used with webapp2.
- """
- assert uri_in_scope.startswith('/')
-
- class RedirectInScope(webapp2.RequestHandler):
- """A handler that redirects to another URI in the same scope."""
-
- def get(self, **_kw):
- """Send the 301/302 response code and write the Location: redirect."""
- split_path = self.request.path.lstrip('/').split('/')
- if len(split_path) > 1:
- project_or_user = split_path[1]
- url = '//%s/%s/%s%s' % (
- self.request.host, scope, project_or_user, uri_in_scope)
- else:
- url = '/'
- if keep_qs and self.request.query_string:
- url += '?' + self.request.query_string
- self.response.location = url
-
- self.response.headers.add('Strict-Transport-Security',
- 'max-age=31536000; includeSubDomains')
- if permanent and not keep_qs:
- self.response.status = http_client.MOVED_PERMANENTLY
- else:
- self.response.status = http_client.FOUND
-
- return RedirectInScope
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)
diff --git a/framework/servlet_helpers.py b/framework/servlet_helpers.py
index fddec26..4d4259c 100644
--- a/framework/servlet_helpers.py
+++ b/framework/servlet_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Helper functions used by the Monorail servlet base class."""
from __future__ import print_function
@@ -22,7 +21,7 @@
from framework import template_helpers
from framework import urls
from framework import xsrf
-from proto import project_pb2
+from mrproto import project_pb2
from google.appengine.api import app_identity
from google.appengine.api import modules
@@ -209,28 +208,37 @@
return (mr.project and mr.project.access != project_pb2.ProjectAccess.ANYONE)
-def SafeCreateLoginURL(mr, continue_url=None):
+def SafeCreateLoginURL(mr):
"""Make a login URL w/ a detailed continue URL, otherwise use a short one."""
- current_page_url = mr.current_page_url_encoded
+ current_url = mr.current_page_url_encoded
if settings.local_mode:
- current_page_url = mr.current_page_url
- continue_url = continue_url or current_page_url
+ current_url = mr.current_page_url
try:
# Check the URL length
- generated_login_url = users.create_login_url(continue_url)
+ generated_login_url = users.create_login_url(current_url)
except users.RedirectTooLongError:
if mr.project_name:
- continue_url = '/p/%s' % mr.project_name
+ current_url = '/p/%s' % mr.project_name
else:
- continue_url = '/'
+ current_url = '/'
if settings.local_mode:
return generated_login_url
+
+ current_parts = urllib.parse.urlparse(current_url)
+ current_query = current_parts.query
+ # Double encode only the query so that it survives redirect parsing.
+ current_url = urllib.parse.urlunparse(
+ current_parts[:3] + ('', urllib.parse.quote_plus(current_query), ''))
# URL to allow user to choose an account when >1 account is logged in.
- redirect_url = (
- 'https://accounts.google.com/AccountChooser?continue='
- 'https://uc.appengine.google.com/_ah/conflogin%3Fcontinue%3D{}')
- url = redirect_url.format(continue_url)
- return url
+ second_redirect_url = 'https://uc.appengine.google.com/_ah/conflogin?'
+ second_redirect_query = 'continue=' + current_url
+ second_redirect_uri = second_redirect_url + second_redirect_query
+
+ first_redirect_url = 'https://accounts.google.com/AccountChooser?'
+ first_redirect_params = {'continue': second_redirect_uri}
+ first_redirect_uri = first_redirect_url + urllib.parse.urlencode(
+ first_redirect_params)
+ return first_redirect_uri
def SafeCreateLogoutURL(mr):
@@ -275,6 +283,6 @@
project_alert = (
'Scheduled for deletion in %d %s.' % (delay_days, days_word))
elif project.state == project_pb2.ProjectState.ARCHIVED:
- project_alert = 'Project is archived: read-only by members only.'
+ project_alert = 'Project is archived and read-only.'
return project_alert
diff --git a/framework/sorting.py b/framework/sorting.py
index 558044c..2ad4c1c 100644
--- a/framework/sorting.py
+++ b/framework/sorting.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Helper functions for sorting lists of project artifacts.
@@ -23,16 +22,16 @@
from __future__ import division
from __future__ import absolute_import
-from functools import total_ordering
+import functools
import settings
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from services import caches
from tracker import tracker_bizobj
from tracker import tracker_constants
-@total_ordering
+@functools.total_ordering
class DescendingValue(object):
"""A wrapper which reverses the sort order of values."""
@@ -149,7 +148,7 @@
art_values_cache.CacheItem(art.issue_id, art_values)
return sort_key
- return sorted(artifacts, key=SortKey)
+ return sorted(artifacts, key=lambda x: Python2Key(SortKey(x)))
def ComputeSortDirectives(config, group_by_spec, sort_spec, tie_breakers=None):
@@ -249,7 +248,7 @@
key_part.extend(value)
else:
key_part.append(value)
- return sorted(key_part)
+ return sorted(key_part, key=Python2Key)
return _MaybeMakeDescending(CombinedAccessor, descending)
@@ -341,7 +340,7 @@
else:
values = [postprocessor(users_by_id[id_or_id_list])]
- return sorted(values) or MAX_STRING
+ return sorted(values) or [MAX_STRING]
return Accessor
@@ -427,10 +426,10 @@
values = base_accessor(art)
if not values:
# Undefined values sort last.
- return MAX_STRING
+ return [MAX_STRING]
indexes = [well_known_value_indexes.get(val, MAX_STRING) for val in values]
- return sorted(indexes)
+ return sorted(indexes, key=Python2Key)
return Accessor
@@ -465,8 +464,8 @@
_SortableApprovalApproverValues(art, approval_fds, users_by_id) +
_SortableLabelValues(art, col_name, well_known_value_indexes))
if not idx_or_lex_list:
- return MAX_STRING # issues with no value sort to the end of the list.
- return sorted(idx_or_lex_list)
+ return [MAX_STRING] # issues with no value sort to the end of the list.
+ return sorted(idx_or_lex_list, key=Python2Key)
return ApproverAccessor
@@ -494,8 +493,8 @@
_SortableFieldValues(art, fd_list, users_by_id, phase_name) +
_SortableLabelValues(art, col_name, well_known_value_indexes))
if not idx_or_lex_list:
- return MAX_STRING # issues with no value sort to the end of the list.
- return sorted(idx_or_lex_list)
+ return [MAX_STRING] # issues with no value sort to the end of the list.
+ return sorted(idx_or_lex_list, key=Python2Key)
return Accessor
@@ -573,3 +572,37 @@
sortable_value_list.append(idx_or_lex)
return sortable_value_list
+
+
+def _Python2Cmp(a, b):
+ """Compares two objects in the Python 2 way.
+
+ In Python 3, comparing two objects of different types raises a TypeError.
+ In Python 2, when you compare two objects of different types, they are
+ generally ordered by their type names, with a few special cases carved
+ out for int/float and str/unicode.
+
+ This comparison function also looks through lists and compares them pairwise.
+ It doesn't do the same for other iterables.
+ """
+ try:
+ # First try comparing the objects directly.
+ # https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
+ return (a > b) - (a < b)
+ except TypeError:
+ s1, s2 = type(a).__name__, type(b).__name__
+ if not (s1 == 'list' and s2 == 'list'):
+ # If they are different types, compare their type names.
+ return (s1 > s2) - (s1 < s2)
+
+ # If they are both lists, compare their elements pairwise.
+ for x, y in zip(a, b):
+ element_cmp = _Python2Cmp(x, y)
+ if element_cmp != 0:
+ return element_cmp
+
+ # If the lists start with the same elements, compare their lengths.
+ return (len(a) > len(b)) - (len(a) < len(b))
+
+
+Python2Key = functools.cmp_to_key(_Python2Cmp)
diff --git a/framework/sql.py b/framework/sql.py
index 0fb8043..d1c45e3 100644
--- a/framework/sql.py
+++ b/framework/sql.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A set of classes for interacting with tables in SQL."""
from __future__ import print_function
@@ -14,7 +13,8 @@
import sys
import time
-from six import string_types
+import six
+from six.moves import queue
import settings
@@ -27,8 +27,6 @@
from infra_libs import ts_mon
-from Queue import Queue
-
class ConnectionPool(object):
"""Manage a set of database connections such that they may be re-used.
@@ -45,15 +43,15 @@
key = instance + '/' + database
if not key in self.queues:
- queue = Queue(self.poolsize)
- self.queues[key] = queue
+ q = queue.Queue(self.poolsize)
+ self.queues[key] = q
- queue = self.queues[key]
+ q = self.queues[key]
- if queue.empty():
+ if q.empty():
cnxn = cnxn_ctor(instance, database)
else:
- cnxn = queue.get()
+ cnxn = q.get()
# Make sure the connection is still good.
cnxn.ping()
cnxn.commit()
@@ -266,13 +264,14 @@
'%d rows in %d ms: %s', cursor.rowcount, int(duration),
formatted_statement)
if duration >= 2000:
- logger.log({
- 'log_type': 'database/query',
- 'statement': formatted_statement,
- 'type': formatted_statement.split(' ')[0],
- 'duration': duration / 1000,
- 'row_count': cursor.rowcount,
- })
+ logger.log(
+ {
+ 'log_type': 'database/query',
+ 'statement': formatted_statement[:100000],
+ 'type': formatted_statement.split(' ')[0],
+ 'duration': duration / 1000,
+ 'row_count': cursor.rowcount,
+ })
if commit and not stmt_str.startswith('SELECT'):
try:
@@ -670,7 +669,7 @@
elif self.limit:
clauses.append('LIMIT %d' % self.limit)
elif self.offset:
- clauses.append('LIMIT %d OFFSET %d' % (sys.maxint, self.offset))
+ clauses.append('LIMIT %d OFFSET %d' % (sys.maxsize, self.offset))
if self.insert_args:
clauses.append('VALUES (' + PlaceHolders(self.insert_args[0]) + ')')
@@ -947,16 +946,19 @@
_MakeRE(r'^LOWER\({tab_col}\) NOT IN \({multi_placeholder}\)$'),
_MakeRE(r'^LOWER\({tab_col}\) LIKE {placeholder}$'),
_MakeRE(r'^LOWER\({tab_col}\) NOT LIKE {placeholder}$'),
- _MakeRE(r'^timestep < \(SELECT MAX\(j.timestep\) FROM Invalidate AS j '
- r'WHERE j.kind = %s '
- r'AND j.cache_key = Invalidate.cache_key\)$'),
- _MakeRE(r'^\({tab_col} IS NULL OR {tab_col} {compare_op} {placeholder}\) '
- 'AND \({tab_col} IS NULL OR {tab_col} {compare_op} {placeholder}'
- '\)$'),
- _MakeRE(r'^\({tab_col} IS NOT NULL AND {tab_col} {compare_op} '
- '{placeholder}\) OR \({tab_col} IS NOT NULL AND {tab_col} '
- '{compare_op} {placeholder}\)$'),
- ]
+ _MakeRE(
+ r'^timestep < \(SELECT MAX\(j.timestep\) FROM Invalidate AS j '
+ r'WHERE j.kind = %s '
+ r'AND j.cache_key = Invalidate.cache_key\)$'),
+ _MakeRE(
+ r'^\({tab_col} IS NULL OR {tab_col} {compare_op} {placeholder}\) '
+ r'AND \({tab_col} IS NULL OR {tab_col} {compare_op} {placeholder}'
+ r'\)$'),
+ _MakeRE(
+ r'^\({tab_col} IS NOT NULL AND {tab_col} {compare_op} '
+ r'{placeholder}\) OR \({tab_col} IS NOT NULL AND {tab_col} '
+ r'{compare_op} {placeholder}\)$'),
+]
# Note: We never use ';' for multiple statements, '@' for SQL variables, or
# any quoted strings in stmt_str (quotes are put in my MySQLdb for args).
@@ -966,7 +968,7 @@
def _IsValidDBValue(val):
- if isinstance(val, string_types):
+ if isinstance(val, six.string_types):
return '\x00' not in val
return True
diff --git a/framework/table_view_helpers.py b/framework/table_view_helpers.py
index 3fa07c2..f81ba40 100644
--- a/framework/table_view_helpers.py
+++ b/framework/table_view_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes and functions for displaying lists of project artifacts.
@@ -24,7 +23,7 @@
from framework import framework_constants
from framework import template_helpers
from framework import timestr
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from tracker import tracker_bizobj
from tracker import tracker_constants
@@ -697,8 +696,8 @@
if not fd:
# TODO(jrobbins): This can happen if an issue with a custom
# field value is moved to a different project.
- logging.warn('Issue ID %r has undefined field value %r',
- art.issue_id, fv)
+ logging.warning(
+ 'Issue ID %r has undefined field value %r', art.issue_id, fv)
elif fd.field_name.lower() == col and (
phase_names_by_id.get(fv.phase_id) == phase_name):
if fd.field_type == tracker_pb2.FieldTypes.URL_TYPE:
@@ -726,8 +725,8 @@
fd = tracker_bizobj.FindFieldDef(col, config)
ad = tracker_bizobj.FindApprovalDef(col, config)
if not (ad and fd):
- logging.warn('Issue ID %r has undefined field value %r',
- art.issue_id, av)
+ logging.warning(
+ 'Issue ID %r has undefined field value %r', art.issue_id, av)
elif av.approval_id == fd.field_id:
explicit_values.append(av.status.name)
break
@@ -745,8 +744,8 @@
fd = tracker_bizobj.FindFieldDef(approval_name, config)
ad = tracker_bizobj.FindApprovalDef(approval_name, config)
if not (ad and fd):
- logging.warn('Issue ID %r has undefined field value %r',
- art.issue_id, av)
+ logging.warning(
+ 'Issue ID %r has undefined field value %r', art.issue_id, av)
elif av.approval_id == fd.field_id:
explicit_values = [users_by_id.get(approver_id).display_name
for approver_id in av.approver_ids
diff --git a/framework/teardown.py b/framework/teardown.py
new file mode 100644
index 0000000..05ebb29
--- /dev/null
+++ b/framework/teardown.py
@@ -0,0 +1,41 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""A handler run on Flask request teardown."""
+
+import logging
+import os
+
+from google.appengine.api import runtime
+from googleapiclient import discovery
+from googleapiclient import errors
+from oauth2client import client
+
+import settings
+
+_MAXIMUM_MEMORY_USAGE = 2000
+
+
+def Teardown(_exc):
+ if settings.local_mode:
+ return
+
+ # Stop the instance if it's using too much memory.
+ memory_usage = runtime.memory_usage().average10m
+ if memory_usage < _MAXIMUM_MEMORY_USAGE:
+ return
+
+ credentials = client.GoogleCredentials.get_application_default()
+ appengine = discovery.build('appengine', 'v1', credentials=credentials)
+ delete = appengine.apps().services().versions().instances().delete(
+ appsId=os.environ.get('GAE_APPLICATION').split('~')[-1],
+ servicesId=os.environ.get('GAE_SERVICE'),
+ versionsId=os.environ.get('GAE_VERSION'),
+ instancesId=os.environ.get('GAE_INSTANCE'))
+ try:
+ delete.execute()
+ except errors.HttpError as e:
+ if e.status_code != 404:
+ raise
+ else:
+ logging.critical('Deleted instance using %d MB of memory.' % memory_usage)
diff --git a/framework/template_helpers.py b/framework/template_helpers.py
index 7ebb7e2..d360054 100644
--- a/framework/template_helpers.py
+++ b/framework/template_helpers.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Some utility classes for interacting with templates."""
@@ -9,12 +8,14 @@
from __future__ import print_function
from __future__ import absolute_import
-import cgi
-import cStringIO
+try:
+ import html
+except ImportError:
+ import cgi as html
from six.moves import http_client
+from six.moves import StringIO
import logging
import time
-import types
import ezt
import six
@@ -119,18 +120,14 @@
return template
-class cStringIOUnicodeWrapper(object):
- """Wrapper on cStringIO.StringIO that encodes unicode as UTF-8 as it goes."""
+class StringIOUnicodeWrapper(object):
+ """Wrapper on io.StringIO that encodes unicode as UTF-8 as it goes."""
def __init__(self):
- self.buffer = cStringIO.StringIO()
+ self.buffer = StringIO()
def write(self, s):
- if isinstance(s, six.text_type):
- utf8_s = s.encode('utf-8')
- else:
- utf8_s = s
- self.buffer.write(utf8_s)
+ self.buffer.write(six.ensure_str(s))
def getvalue(self):
return self.buffer.getvalue()
@@ -157,20 +154,6 @@
if content_type:
response.content_type = content_type
- response.status = data.get('http_response_code', http_client.OK)
- whole_page = self.GetResponse(data)
- if data.get('prevent_sniffing'):
- for sniff_pattern, sniff_replacement in SNIFFABLE_PATTERNS.items():
- whole_page = whole_page.replace(sniff_pattern, sniff_replacement)
- start = time.time()
- response.write(whole_page)
- logging.info('wrote response in %dms', int((time.time() - start) * 1000))
-
- def WriteFlaskResponse(self, response, data, content_type=None):
- """Write the parsed and filled in template to http server."""
- if content_type:
- response.content_type = content_type
-
response.status_code = data.get('http_response_code', http_client.OK)
whole_page = self.GetResponse(data)
if data.get('prevent_sniffing'):
@@ -184,7 +167,7 @@
"""Generate the text from the template and return it as a string."""
template = self.GetTemplate()
start = time.time()
- buf = cStringIOUnicodeWrapper()
+ buf = StringIOUnicodeWrapper()
template.generate(buf, data)
whole_page = buf.getvalue()
logging.info('rendering took %dms', int((time.time() - start) * 1000))
@@ -311,7 +294,7 @@
page_data: Template data which may include a 'labels' field.
"""
label_list = page_data.get('labels', [])
- if isinstance(label_list, types.StringTypes):
+ if isinstance(label_list, six.string_types):
label_list = [label.strip() for label in page_data['labels'].split(',')]
for i in range(len(label_list)):
@@ -334,7 +317,7 @@
"""Return a string that can be used in an HTML email body."""
if self.tag == 'a' and self.href:
return '<a href="%s">%s</a>' % (
- cgi.escape(self.href, quote=True),
- cgi.escape(self.content, quote=True))
+ html.escape(self.href,
+ quote=True), html.escape(self.content, quote=True))
- return cgi.escape(self.content, quote=True)
+ return html.escape(self.content, quote=True)
diff --git a/framework/test/alerts_test.py b/framework/test/alerts_test.py
index 0c398c1..122a0fe 100644
--- a/framework/test/alerts_test.py
+++ b/framework/test/alerts_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for alert display helpers."""
from __future__ import print_function
diff --git a/framework/test/authdata_test.py b/framework/test/authdata_test.py
index a0e7313..22a7552 100644
--- a/framework/test/authdata_test.py
+++ b/framework/test/authdata_test.py
@@ -1,7 +1,6 @@
-# Copyright 2017 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 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the authdata module."""
from __future__ import print_function
diff --git a/framework/test/banned_test.py b/framework/test/banned_test.py
index 0331cdd..f07b6e8 100644
--- a/framework/test/banned_test.py
+++ b/framework/test/banned_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unittests for monorail.framework.banned."""
from __future__ import print_function
@@ -10,8 +9,6 @@
import unittest
-import webapp2
-
from framework import banned
from framework import monorailrequest
from services import service_manager
diff --git a/framework/test/cloud_tasks_helpers_test.py b/framework/test/cloud_tasks_helpers_test.py
index 09ad2cd..bbc52b0 100644
--- a/framework/test/cloud_tasks_helpers_test.py
+++ b/framework/test/cloud_tasks_helpers_test.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for the cloud tasks helper module."""
@@ -34,8 +34,8 @@
get_client_mock().queue_path.assert_called_with(
settings.app_id, settings.CLOUD_TASKS_REGION, queue)
get_client_mock().create_task.assert_called_once()
- ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
- self.assertEqual(called_task, task)
+ _, kwargs = get_client_mock().create_task.call_args
+ self.assertEqual(kwargs['task'], task)
@mock.patch('framework.cloud_tasks_helpers._get_client')
def test_create_task_raises(self, get_client_mock):
@@ -53,7 +53,7 @@
cloud_tasks_helpers.create_task(task)
- (_args, kwargs) = get_client_mock().create_task.call_args
+ _, kwargs = get_client_mock().create_task.call_args
self.assertEqual(kwargs.get('retry'), cloud_tasks_helpers._DEFAULT_RETRY)
def test_generate_simple_task(self):
@@ -66,7 +66,7 @@
'app_engine_http_request':
{
'relative_uri': '/alphabet/letters',
- 'body': 'a=a&b=b',
+ 'body': b'a=a&b=b',
'headers': {
'Content-type': 'application/x-www-form-urlencoded'
}
@@ -79,7 +79,7 @@
'app_engine_http_request':
{
'relative_uri': '/alphabet/letters',
- 'body': '',
+ 'body': b'',
'headers': {
'Content-type': 'application/x-www-form-urlencoded'
}
diff --git a/framework/test/csv_helpers_test.py b/framework/test/csv_helpers_test.py
index 19c89c5..a8726a7 100644
--- a/framework/test/csv_helpers_test.py
+++ b/framework/test/csv_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for csv_helpers functions."""
from __future__ import print_function
diff --git a/framework/test/deleteusers_test.py b/framework/test/deleteusers_test.py
index 87ed5bc..13c6922 100644
--- a/framework/test/deleteusers_test.py
+++ b/framework/test/deleteusers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2019 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 2019 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for deleteusers classes."""
from __future__ import print_function
@@ -10,6 +9,7 @@
import logging
import mock
+import six
import unittest
from six.moves import urllib
@@ -52,24 +52,24 @@
self.assertEqual(get_client_mock().create_task.call_count, 3)
expected_task = self.generate_simple_task(
- urls.SEND_WIPEOUT_USER_LISTS_TASK + '.do', 'limit=2&offset=0')
+ urls.SEND_WIPEOUT_USER_LISTS_TASK + '.do', b'limit=2&offset=0')
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
expected_task = self.generate_simple_task(
- urls.SEND_WIPEOUT_USER_LISTS_TASK + '.do', 'limit=2&offset=2')
+ urls.SEND_WIPEOUT_USER_LISTS_TASK + '.do', b'limit=2&offset=2')
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
expected_task = self.generate_simple_task(
- urls.DELETE_WIPEOUT_USERS_TASK + '.do', '')
+ urls.DELETE_WIPEOUT_USERS_TASK + '.do', b'')
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
@mock.patch('framework.cloud_tasks_helpers._get_client')
@@ -79,10 +79,10 @@
expected_task = self.generate_simple_task(
urls.SEND_WIPEOUT_USER_LISTS_TASK + '.do',
- 'limit={}&offset=0'.format(deleteusers.MAX_BATCH_SIZE))
+ b'limit=%d&offset=0' % deleteusers.MAX_BATCH_SIZE)
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
@mock.patch('framework.cloud_tasks_helpers._get_client')
@@ -118,13 +118,13 @@
def testHandleRequest_NoLimit(self):
mr = testing_helpers.MakeMonorailRequest()
self.services.user.users_by_id = {}
- with self.assertRaisesRegexp(AssertionError, 'Missing param limit'):
+ with self.assertRaisesRegex(AssertionError, 'Missing param limit'):
self.task.HandleRequest(mr)
def testHandleRequest_NoOffset(self):
mr = testing_helpers.MakeMonorailRequest(path='url/url?limit=3')
self.services.user.users_by_id = {}
- with self.assertRaisesRegexp(AssertionError, 'Missing param offset'):
+ with self.assertRaisesRegex(AssertionError, 'Missing param offset'):
self.task.HandleRequest(mr)
def testHandleRequest_ZeroOffset(self):
@@ -152,7 +152,7 @@
'app_engine_http_request':
{
'relative_uri': url,
- 'body': body,
+ 'body': six.ensure_binary(body),
'headers': {
'Content-type': 'application/x-www-form-urlencoded'
}
@@ -178,8 +178,8 @@
urls.DELETE_USERS_TASK + '.do', query)
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
query = urllib.parse.urlencode({'emails': 'user4@gmail.com'})
@@ -187,8 +187,8 @@
urls.DELETE_USERS_TASK + '.do', query)
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
@mock.patch('framework.cloud_tasks_helpers._get_client')
@@ -206,6 +206,6 @@
urls.DELETE_USERS_TASK + '.do', query)
get_client_mock().create_task.assert_any_call(
- get_client_mock().queue_path(),
- expected_task,
+ parent=get_client_mock().queue_path(),
+ task=expected_task,
retry=cloud_tasks_helpers._DEFAULT_RETRY)
diff --git a/framework/test/emailfmt_test.py b/framework/test/emailfmt_test.py
index dd7cca3..5445189 100644
--- a/framework/test/emailfmt_test.py
+++ b/framework/test/emailfmt_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for monorail.framework.emailfmt."""
from __future__ import print_function
@@ -9,6 +8,7 @@
from __future__ import absolute_import
import mock
+import six
import unittest
from google.appengine.ext import testbed
@@ -16,7 +16,7 @@
import settings
from framework import emailfmt
from framework import framework_views
-from proto import project_pb2
+from mrproto import project_pb2
from testing import testing_helpers
from google.appengine.api import apiproxy_stub_map
@@ -79,12 +79,11 @@
testing_helpers.HEADER_LINES + [references_header], 'awesome!')
(from_addr, to_addrs, cc_addrs, references, incident_id, subject,
body) = emailfmt.ParseEmailMessage(msg)
- self.assertItemsEqual(
- ['<5678@bar.com>',
- '<0=969704940193871313=13442892928193434663='
- 'proj@monorail.example.com>',
- '<1234@foo.com>'],
- references)
+ six.assertCountEqual(
+ self, [
+ '<5678@bar.com>', '<0=969704940193871313=13442892928193434663='
+ 'proj@monorail.example.com>', '<1234@foo.com>'
+ ], references)
def testParseEmailMessage_Bulk(self):
for precedence in ['Bulk', 'Junk']:
diff --git a/framework/test/exceptions_test.py b/framework/test/exceptions_test.py
index 8fe2295..5013267 100644
--- a/framework/test/exceptions_test.py
+++ b/framework/test/exceptions_test.py
@@ -1,6 +1,6 @@
-# Copyright 2020 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.
+# Copyright 2020 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unittest for the exceptions module."""
from __future__ import print_function
@@ -21,7 +21,7 @@
err_aggregator.AddErrorMessage('The chickens are missing.')
err_aggregator.AddErrorMessage('The foxes are free.')
- with self.assertRaisesRegexp(
+ with self.assertRaisesRegex(
exceptions.InputException,
'The chickens are missing.\nThe foxes are free.'):
err_aggregator.RaiseIfErrors()
@@ -34,16 +34,16 @@
def testWithinContext_ExceptionPassedIn(self):
"""We do not suppress exceptions raised within wrapped code."""
- with self.assertRaisesRegexp(exceptions.InputException,
- 'We should raise this'):
+ with self.assertRaisesRegex(exceptions.InputException,
+ 'We should raise this'):
with exceptions.ErrorAggregator(exceptions.InputException) as errors:
errors.AddErrorMessage('We should ignore this error.')
raise exceptions.InputException('We should raise this')
def testWithinContext_NoExceptionPassedIn(self):
"""We raise an exception for any errors if no exceptions are passed in."""
- with self.assertRaisesRegexp(exceptions.InputException,
- 'We can raise this now.'):
+ with self.assertRaisesRegex(exceptions.InputException,
+ 'We can raise this now.'):
with exceptions.ErrorAggregator(exceptions.InputException) as errors:
errors.AddErrorMessage('We can raise this now.')
return True
diff --git a/framework/test/filecontent_test.py b/framework/test/filecontent_test.py
index 4843b47..f2c8d9e 100644
--- a/framework/test/filecontent_test.py
+++ b/framework/test/filecontent_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for the filecontent module."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
+import six
import unittest
from framework import filecontent
@@ -84,10 +84,10 @@
return is_binary
def testFileIsBinaryEmpty(self):
- self.assertFalse(self.IsBinary(''))
+ self.assertFalse(self.IsBinary(b''))
def testFileIsBinaryShortText(self):
- self.assertFalse(self.IsBinary('This is some plain text.'))
+ self.assertFalse(self.IsBinary(b'This is some plain text.'))
def testLineLengthDetection(self):
unicode_str = (
@@ -101,29 +101,35 @@
lines.append(long_line)
# High lower ratio - text
- self.assertFalse(self.IsBinary('\n'.join(lines)))
+ self.assertFalse(self.IsBinary(b'\n'.join(lines)))
lines.extend([long_line] * 99)
# 50/50 lower/upper ratio - binary
- self.assertTrue(self.IsBinary('\n'.join(lines)))
+ self.assertTrue(self.IsBinary(b'\n'.join(lines)))
# Single line too long - binary
lines = [short_line] * 100
lines.append(short_line * 100) # Very long line
- self.assertTrue(self.IsBinary('\n'.join(lines)))
+ self.assertTrue(self.IsBinary(b'\n'.join(lines)))
def testFileIsBinaryLongText(self):
- self.assertFalse(self.IsBinary('This is plain text. \n' * 100))
+ self.assertFalse(self.IsBinary(b'This is plain text. \n' * 100))
# long utf-8 lines are OK
- self.assertFalse(self.IsBinary('This one long line. ' * 100))
+ self.assertFalse(self.IsBinary(b'This one long line. ' * 100))
def testFileIsBinaryLongBinary(self):
- bin_string = ''.join([chr(c) for c in range(122, 252)])
+ if six.PY2:
+ bin_string = ''.join([chr(c) for c in range(122, 252)])
+ else:
+ bin_string = bytes(range(122, 252))
self.assertTrue(self.IsBinary(bin_string * 100))
def testFileIsTextByPath(self):
- bin_string = ''.join([chr(c) for c in range(122, 252)] * 100)
+ if six.PY2:
+ bin_string = ''.join([chr(c) for c in range(122, 252)] * 100)
+ else:
+ bin_string = bytes(range(122, 252)) * 100
unicode_str = (
u'Some non-ascii chars - '
u'\xa2\xfa\xb6\xe7\xfc\xea\xd0\xf4\xe6\xf0\xce\xf6\xbe')
@@ -143,7 +149,7 @@
filecontent.DecodeFileContents(contents, path=path)[1])
def testFileIsBinaryByCommonExtensions(self):
- contents = 'this is not examined'
+ contents = b'this is not examined'
self.assertTrue(filecontent.DecodeFileContents(
contents, path='junk.zip')[1])
self.assertTrue(filecontent.DecodeFileContents(
@@ -175,13 +181,13 @@
contents, path='/wiki/PageName.wiki')[1])
def testUnreasonablyLongFile(self):
- contents = '\n' * (filecontent.SOURCE_FILE_MAX_LINES + 2)
+ contents = b'\n' * (filecontent.SOURCE_FILE_MAX_LINES + 2)
_contents, is_binary, is_long = filecontent.DecodeFileContents(
contents)
self.assertFalse(is_binary)
self.assertTrue(is_long)
- contents = '\n' * 100
+ contents = b'\n' * 100
_contents, is_binary, is_long = filecontent.DecodeFileContents(
contents)
self.assertFalse(is_binary)
diff --git a/framework/test/flask_servlet_test.py b/framework/test/flask_servlet_test.py
deleted file mode 100644
index 4c47209..0000000
--- a/framework/test/flask_servlet_test.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# 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
-"""Unit tests for servlet base class module."""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-
-import time
-import mock
-import unittest
-import logging
-
-from google.appengine.ext import testbed
-
-from framework import flaskservlet, framework_constants, servlet_helpers
-from framework import xsrf
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
-from services import service_manager
-from testing import fake
-from testing import testing_helpers
-
-
-class TestableFlaskServlet(flaskservlet.FlaskServlet):
- """A tiny concrete subclass of abstract class Servlet."""
-
- def __init__(self, services=None, do_post_redirect=True):
- super(TestableFlaskServlet, self).__init__(services=services)
- self.do_post_redirect = do_post_redirect
- self.seen_post_data = None
-
-
-class FlaskServletTest(unittest.TestCase):
-
- def setUp(self):
- services = service_manager.Services(
- project=fake.ProjectService(),
- project_star=fake.ProjectStarService(),
- user=fake.UserService(),
- usergroup=fake.UserGroupService())
- services.user.TestAddUser('user@example.com', 111)
- self.page_class = flaskservlet.FlaskServlet(services=services)
- self.testbed = testbed.Testbed()
- self.testbed.activate()
- self.testbed.init_user_stub()
- self.testbed.init_memcache_stub()
- self.testbed.init_datastore_v3_stub()
-
- def tearDown(self):
- self.testbed.deactivate()
-
- def testDefaultValues(self):
- self.assertEqual(None, self.page_class._MAIN_TAB_MODE)
- self.assertTrue(self.page_class._TEMPLATE_PATH.endswith('/templates/'))
- self.assertEqual(None, self.page_class._PAGE_TEMPLATE)
-
- @mock.patch('flask.abort')
- def testCheckForMovedProject_NoRedirect(self, mock_abort):
- project = fake.Project(
- project_name='proj', state=project_pb2.ProjectState.LIVE)
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- self.page_class._CheckForMovedProject(mr, request)
- mock_abort.assert_not_called()
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/source/browse/p/adminAdvanced', project=project)
- self.page_class._CheckForMovedProject(mr, request)
- mock_abort.assert_not_called()
-
- @mock.patch('flask.redirect')
- def testCheckForMovedProject_Redirect(self, mock_redirect):
- project = fake.Project(project_name='proj', moved_to='http://example.com')
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- self.page_class.request_path = '/p/test'
- self.page_class._CheckForMovedProject(mr, request)
- mock_redirect.assert_called_once_with(
- 'http://127.0.0.1/hosting/moved?project=proj', code=302)
-
- def testGatherBaseData(self):
- project = self.page_class.services.project.TestAddProject(
- 'testproj', state=project_pb2.ProjectState.LIVE)
- project.cached_content_timestamp = 12345
-
- (_request, mr) = testing_helpers.GetRequestObjects(
- path='/p/testproj/feeds', project=project)
- nonce = '1a2b3c4d5e6f7g'
-
- base_data = self.page_class.GatherBaseData(mr, nonce)
-
- self.assertEqual(base_data['nonce'], nonce)
- self.assertEqual(base_data['projectname'], 'testproj')
- self.assertEqual(base_data['project'].cached_content_timestamp, 12345)
- self.assertEqual(base_data['project_alert'], None)
-
- self.assertTrue(base_data['currentPageURL'].endswith('/p/testproj/feeds'))
- self.assertTrue(
- base_data['currentPageURLEncoded'].endswith('%2Fp%2Ftestproj%2Ffeeds'))
-
- def testGatherHelpData_Normal(self):
- project = fake.Project(project_name='proj')
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual(None, help_data['cue'])
- self.assertEqual(None, help_data['account_cue'])
diff --git a/framework/test/framework_bizobj_test.py b/framework/test/framework_bizobj_test.py
index 131ebb5..5ea05a6 100644
--- a/framework/test/framework_bizobj_test.py
+++ b/framework/test/framework_bizobj_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for monorail.framework.framework_bizobj."""
from __future__ import print_function
@@ -15,9 +14,9 @@
from framework import authdata
from framework import framework_bizobj
from framework import framework_constants
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
from services import service_manager
from services import client_config_svc
from testing import fake
diff --git a/framework/test/framework_helpers_test.py b/framework/test/framework_helpers_test.py
index fb8810b..fe0a225 100644
--- a/framework/test/framework_helpers_test.py
+++ b/framework/test/framework_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the framework_helpers module."""
from __future__ import print_function
@@ -15,14 +14,15 @@
from mox3 import mox
except ImportError:
import mox
+import six
import time
from businesslogic import work_env
from framework import framework_helpers
from framework import framework_views
-from proto import features_pb2
-from proto import project_pb2
-from proto import user_pb2
+from mrproto import features_pb2
+from mrproto import project_pb2
+from mrproto import user_pb2
from services import service_manager
from testing import fake
from testing import testing_helpers
@@ -381,8 +381,8 @@
"""Run one call to the target method and check expected results."""
actual_added, actual_removed = framework_helpers.ComputeListDeltas(
old, new)
- self.assertItemsEqual(added, actual_added)
- self.assertItemsEqual(removed, actual_removed)
+ six.assertCountEqual(self, added, actual_added)
+ six.assertCountEqual(self, removed, actual_removed)
def testEmptyLists(self):
self.DoOne(old=[], new=[], added=[], removed=[])
@@ -430,7 +430,7 @@
'preview_on_hover',
'settings_user_prefs',
]
- self.assertItemsEqual(expected_keys, list(page_data.keys()))
+ six.assertCountEqual(self, expected_keys, list(page_data.keys()))
self.assertEqual('profile/url', page_data['profile_url_fragment'])
self.assertTrue(page_data['settings_user_prefs'].public_issue_notice)
diff --git a/framework/test/framework_views_test.py b/framework/test/framework_views_test.py
index 57f9fd1..edb05cd 100644
--- a/framework/test/framework_views_test.py
+++ b/framework/test/framework_views_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for framework_views classes."""
from __future__ import print_function
@@ -14,9 +13,9 @@
from framework import framework_constants
from framework import framework_views
from framework import monorailrequest
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
import settings
from services import service_manager
from testing import fake
diff --git a/framework/test/gcs_helpers_test.py b/framework/test/gcs_helpers_test.py
index a7c01d0..5eab073 100644
--- a/framework/test/gcs_helpers_test.py
+++ b/framework/test/gcs_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the framework_helpers module."""
from __future__ import print_function
@@ -27,8 +26,7 @@
self.testbed.init_memcache_stub()
self.testbed.init_app_identity_stub()
- self.test_storage_client = mock.create_autospec(
- storage.Client, instance=True)
+ self.test_storage_client = mock.MagicMock()
mock.patch.object(
storage, 'Client', return_value=self.test_storage_client).start()
diff --git a/framework/test/grid_view_helpers_test.py b/framework/test/grid_view_helpers_test.py
index df3ecc6..d54353a 100644
--- a/framework/test/grid_view_helpers_test.py
+++ b/framework/test/grid_view_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for grid_view_helpers classes and functions."""
from __future__ import print_function
@@ -13,7 +12,7 @@
from framework import framework_constants
from framework import framework_views
from framework import grid_view_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from testing import fake
from tracker import tracker_bizobj
diff --git a/framework/test/jsonfeed_test.py b/framework/test/jsonfeed_test.py
index 4ca83fa..7c9a6b6 100644
--- a/framework/test/jsonfeed_test.py
+++ b/framework/test/jsonfeed_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for jsonfeed module."""
from __future__ import print_function
@@ -9,13 +8,12 @@
from __future__ import absolute_import
from six.moves import http_client
-import logging
import unittest
+import flask
from google.appengine.api import app_identity
from framework import jsonfeed
-from framework import servlet
from framework import xsrf
from services import service_manager
from testing import testing_helpers
@@ -28,7 +26,7 @@
def testGet(self):
"""Tests handling of GET requests."""
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
# all expected args are present + a bonus arg that should be ignored
feed.mr = testing_helpers.MakeMonorailRequest(
@@ -41,7 +39,7 @@
def testPost(self):
"""Tests handling of POST requests."""
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.mr = testing_helpers.MakeMonorailRequest(
path='/foo/bar/wee?sna=foo', method='POST',
params={'a': '123', 'z': 'zebra'})
@@ -52,7 +50,7 @@
self.assertEqual(1, len(feed.json_data))
def testSecurityTokenChecked_BadToken(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.mr = testing_helpers.MakeMonorailRequest(
user_info={'user_id': 555})
# Note that feed.mr has no token set.
@@ -64,7 +62,7 @@
self.assertRaises(xsrf.TokenIncorrect, feed.post)
def testSecurityTokenChecked_HandlerDoesNotNeedToken(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.mr = testing_helpers.MakeMonorailRequest(
user_info={'user_id': 555})
# Note that feed.mr has no token set.
@@ -73,21 +71,21 @@
feed.post()
def testSecurityTokenChecked_AnonUserDoesNotNeedToken(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.mr = testing_helpers.MakeMonorailRequest()
# Note that feed.mr has no token set, but also no auth.user_id.
feed.get()
feed.post()
def testSameAppOnly_ExternallyAccessible(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.mr = testing_helpers.MakeMonorailRequest()
# Note that request has no X-Appengine-Inbound-Appid set.
feed.get()
feed.post()
def testSameAppOnly_InternalOnlyCalledFromSameApp(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.CHECK_SAME_APP = True
feed.mr = testing_helpers.MakeMonorailRequest()
app_id = app_identity.get_application_id()
@@ -96,36 +94,36 @@
feed.post()
def testSameAppOnly_InternalOnlyCalledExternally(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.CHECK_SAME_APP = True
feed.mr = testing_helpers.MakeMonorailRequest()
# Note that request has no X-Appengine-Inbound-Appid set.
+ feed.response = flask.Response()
self.assertIsNone(feed.get())
self.assertFalse(feed.handle_request_called)
- self.assertEqual(http_client.FORBIDDEN, feed.response.status)
+ self.assertEqual(http_client.FORBIDDEN, feed.response.status_code)
self.assertIsNone(feed.post())
self.assertFalse(feed.handle_request_called)
- self.assertEqual(http_client.FORBIDDEN, feed.response.status)
+ self.assertEqual(http_client.FORBIDDEN, feed.response.status_code)
def testSameAppOnly_InternalOnlyCalledFromWrongApp(self):
- feed = TestableJsonFeed()
+ feed = _TestableJsonFeed()
feed.CHECK_SAME_APP = True
feed.mr = testing_helpers.MakeMonorailRequest()
feed.mr.request.headers['X-Appengine-Inbound-Appid'] = 'wrong'
+ feed.response = flask.Response()
self.assertIsNone(feed.get())
self.assertFalse(feed.handle_request_called)
- self.assertEqual(http_client.FORBIDDEN, feed.response.status)
+ self.assertEqual(http_client.FORBIDDEN, feed.response.status_code)
self.assertIsNone(feed.post())
self.assertFalse(feed.handle_request_called)
- self.assertEqual(http_client.FORBIDDEN, feed.response.status)
+ self.assertEqual(http_client.FORBIDDEN, feed.response.status_code)
-class TestableJsonFeed(jsonfeed.JsonFeed):
+class _TestableJsonFeed(jsonfeed.JsonFeed):
- def __init__(self, request=None):
- response = testing_helpers.Blank()
- super(TestableJsonFeed, self).__init__(
- request or 'req', response, services=service_manager.Services())
+ def __init__(self):
+ super(_TestableJsonFeed, self).__init__(services=service_manager.Services())
self.response_data = None
self.handle_request_called = False
diff --git a/framework/test/monitoring_test.py b/framework/test/monitoring_test.py
index edbd15d..0592336 100644
--- a/framework/test/monitoring_test.py
+++ b/framework/test/monitoring_test.py
@@ -1,6 +1,6 @@
-# Copyright 2020 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.
+# Copyright 2020 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the monitoring module."""
diff --git a/framework/test/monorailcontext_test.py b/framework/test/monorailcontext_test.py
index 2071c9e..04ef491 100644
--- a/framework/test/monorailcontext_test.py
+++ b/framework/test/monorailcontext_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 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 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for MonorailContext."""
from __future__ import print_function
diff --git a/framework/test/monorailrequest_test.py b/framework/test/monorailrequest_test.py
index ef52f1e..0c81694 100644
--- a/framework/test/monorailrequest_test.py
+++ b/framework/test/monorailrequest_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the monorailrequest module."""
from __future__ import print_function
@@ -18,17 +17,16 @@
except ImportError:
import mox
import six
+import werkzeug
from google.appengine.api import oauth
from google.appengine.api import users
-import webapp2
-
from framework import exceptions
from framework import monorailrequest
from framework import permissions
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
from services import service_manager
from testing import fake
from testing import testing_helpers
@@ -170,14 +168,15 @@
def testGetIntListParam_NoParam(self):
mr = monorailrequest.MonorailRequest(self.services)
- mr.ParseRequest(webapp2.Request.blank('servlet'), self.services)
+ mr.ParseRequest(testing_helpers.RequestStub('servlet'), self.services)
self.assertEqual(mr.GetIntListParam('ids'), None)
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
['test'])
def testGetIntListParam_OneValue(self):
mr = monorailrequest.MonorailRequest(self.services)
- mr.ParseRequest(webapp2.Request.blank('servlet?ids=11'), self.services)
+ request = testing_helpers.RequestStub('servlet?ids=11')
+ mr.ParseRequest(request, self.services)
self.assertEqual(mr.GetIntListParam('ids'), [11])
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
[11])
@@ -185,7 +184,7 @@
def testGetIntListParam_MultiValue(self):
mr = monorailrequest.MonorailRequest(self.services)
mr.ParseRequest(
- webapp2.Request.blank('servlet?ids=21,22,23'), self.services)
+ testing_helpers.RequestStub('servlet?ids=21,22,23'), self.services)
self.assertEqual(mr.GetIntListParam('ids'), [21, 22, 23])
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
[21, 22, 23])
@@ -194,18 +193,18 @@
mr = monorailrequest.MonorailRequest(self.services)
with self.assertRaises(exceptions.InputException):
mr.ParseRequest(
- webapp2.Request.blank('servlet?ids=not_an_int'), self.services)
+ testing_helpers.RequestStub('servlet?ids=not_an_int'), self.services)
def testGetIntListParam_Malformed(self):
mr = monorailrequest.MonorailRequest(self.services)
with self.assertRaises(exceptions.InputException):
mr.ParseRequest(
- webapp2.Request.blank('servlet?ids=31,32,,'), self.services)
+ testing_helpers.RequestStub('servlet?ids=31,32,,'), self.services)
def testDefaultValuesNoUrl(self):
"""If request has no param, default param values should be used."""
mr = monorailrequest.MonorailRequest(self.services)
- mr.ParseRequest(webapp2.Request.blank('servlet'), self.services)
+ mr.ParseRequest(testing_helpers.RequestStub('servlet'), self.services)
self.assertEqual(mr.GetParam('r', 3), 3)
self.assertEqual(mr.GetIntParam('r', 3), 3)
self.assertEqual(mr.GetPositiveIntParam('r', 3), 3)
@@ -213,7 +212,7 @@
def _MRWithMockRequest(
self, path, headers=None, *mr_args, **mr_kwargs):
- request = webapp2.Request.blank(path, headers=headers)
+ request = testing_helpers.RequestStub(path, headers=headers)
mr = monorailrequest.MonorailRequest(self.services, *mr_args, **mr_kwargs)
mr.ParseRequest(request, self.services)
return mr
@@ -282,7 +281,7 @@
mr.viewed_user_auth.user_pb)
def testViewedUser_NoSuchEmail(self):
- with self.assertRaises(webapp2.HTTPException) as cm:
+ with self.assertRaises(werkzeug.exceptions.HTTPException) as cm:
self._MRWithMockRequest('/u/unknownuser@example.com/')
self.assertEqual(404, cm.exception.code)
@@ -353,13 +352,13 @@
# project colspec contains hotlist columns
mr = testing_helpers.MakeMonorailRequest(
- path='p/proj/issues/detail?id=123&colspec=Rank Adder Adder Owner')
+ path='/p/proj/issues/detail?id=123&colspec=Rank Adder Adder Owner')
mr.ComputeColSpec(None)
self.assertEqual(tracker_constants.DEFAULT_COL_SPEC, mr.col_spec)
# hotlist columns are not deleted when page is a hotlist page
mr = testing_helpers.MakeMonorailRequest(
- path='u/jrobbins@example.com/hotlists/TestHotlist?colspec=Rank Adder',
+ path='/u/jrobbins@example.com/hotlists/TestHotlist?colspec=Rank Adder',
hotlist=self.hotlist)
mr.ComputeColSpec(None)
self.assertEqual('Rank Adder', mr.col_spec)
@@ -513,10 +512,13 @@
self.assertEqual(['Foo-Bar', 'Foo-Bar-Baz', 'Release-1.2', 'Hey', 'There'],
parse('Foo-Bar Foo-Bar-Baz Release-1.2 Hey!There'))
self.assertEqual(
- ['\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9'.decode('utf-8'),
- '\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')],
- parse('\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9 '
- '\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')))
+ [
+ b'\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9'.decode('utf-8'),
+ b'\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')
+ ],
+ parse(
+ b'\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9 '
+ b'\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')))
def testParseColSpec_Dedup(self):
"""An attacker cannot inflate response size by repeating a column."""
@@ -584,7 +586,7 @@
email=lambda: email))
self.mox.ReplayAll()
- request = webapp2.Request.blank('/p/' + project_name)
+ request = testing_helpers.RequestStub('/p/' + project_name)
mr = monorailrequest.MonorailRequest(self.services)
with mr.profiler.Phase('parse user info'):
mr.ParseRequest(request, self.services)
@@ -609,7 +611,7 @@
def testExternalUserPermissions_Archived(self):
mr = self.MakeRequestAsUser('archived', 'user@gmail.com')
- self.CheckPermissions(mr.perms, False, False, False)
+ self.CheckPermissions(mr.perms, True, False, False)
def testExternalUserPermissions_MembersOnly(self):
mr = self.MakeRequestAsUser('members-only', 'user@gmail.com')
diff --git a/framework/test/paginate_test.py b/framework/test/paginate_test.py
index 99adaa9..44c8b59 100644
--- a/framework/test/paginate_test.py
+++ b/framework/test/paginate_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for pagination classes."""
from __future__ import print_function
@@ -15,7 +14,7 @@
from framework import exceptions
from framework import paginate
from testing import testing_helpers
-from proto import secrets_pb2
+from mrproto import secrets_pb2
class PageTokenTest(unittest.TestCase):
diff --git a/framework/test/permissions_test.py b/framework/test/permissions_test.py
index cd67c6c..6280208 100644
--- a/framework/test/permissions_test.py
+++ b/framework/test/permissions_test.py
@@ -1,13 +1,13 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for permissions.py."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
+import six
import time
import unittest
@@ -21,12 +21,13 @@
from framework import framework_constants
from framework import framework_views
from framework import permissions
-from proto import features_pb2
-from proto import project_pb2
-from proto import site_pb2
-from proto import tracker_pb2
-from proto import user_pb2
-from proto import usergroup_pb2
+from mrproto import features_pb2
+from mrproto import project_pb2
+from mrproto import site_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
+from mrproto import usergroup_pb2
+from services import service_manager
from testing import fake
from testing import testing_helpers
from tracker import tracker_bizobj
@@ -160,10 +161,20 @@
self.assertEqual('PermissionSet(a, b, cc)', self.perms.DebugString())
def testRepr(self):
- self.assertEqual('PermissionSet(frozenset([]))',
- permissions.PermissionSet([]).__repr__())
- self.assertEqual('PermissionSet(frozenset([\'a\']))',
- permissions.PermissionSet(['A']).__repr__())
+ if six.PY2:
+ self.assertEqual(
+ 'PermissionSet(frozenset([]))',
+ permissions.PermissionSet([]).__repr__())
+ self.assertEqual(
+ "PermissionSet(frozenset(['a']))",
+ permissions.PermissionSet(['A']).__repr__())
+ else:
+ self.assertEqual(
+ 'PermissionSet(frozenset())',
+ permissions.PermissionSet([]).__repr__())
+ self.assertEqual(
+ "PermissionSet(frozenset({'a'}))",
+ permissions.PermissionSet(['A']).__repr__())
class PermissionsTest(unittest.TestCase):
@@ -953,8 +964,9 @@
None, None, self.live_project))
self.archived_project.delete_time = self.NOW + 1
- self.assertFalse(permissions.UserCanViewProject(
- None, None, self.archived_project))
+ # Anonymous users may view an archived project.
+ self.assertTrue(
+ permissions.UserCanViewProject(None, None, self.archived_project))
self.assertTrue(permissions.UserCanViewProject(
self.owner, {self.OWNER_USER_ID}, self.archived_project))
self.assertTrue(permissions.UserCanViewProject(
@@ -1230,6 +1242,14 @@
ADMIN_PERMS = permissions.ADMIN_PERMISSIONSET
PERMS = permissions.EMPTY_PERMISSIONSET
+ def setUp(self):
+ self.user_svc = fake.UserService()
+ self.services = service_manager.Services(user=self.user_svc)
+
+ self.user_svc.TestAddUser('allowlisteduser@test.com', 567)
+
+ settings.config_freeze_project_ids = {}
+
def testUpdateIssuePermissions_Normal(self):
perms = permissions.UpdateIssuePermissions(
permissions.COMMITTER_ACTIVE_PERMISSIONSET, self.PROJECT,
@@ -1366,6 +1386,21 @@
self.RESTRICTED_ISSUE3, {OWNER_ID})
self.assertIn('editissue', perms.perm_names)
+ def testUpdateIssuePermissions_DefaultPermsDoNotIncludeEdit(self):
+ # Permissions can be checked from the homepage without a project context.
+ perms = permissions.UpdateIssuePermissions(
+ permissions.COMMITTER_ACTIVE_PERMISSIONSET, None,
+ self.RESTRICTED_ISSUE3, {OWNER_ID})
+ self.assertNotIn('editissue', perms.perm_names)
+
+ def testUpdateIssuePermissions_OwnerRespectsArchivedProject(self):
+ project = project_pb2.Project()
+ project.state = project_pb2.ProjectState.ARCHIVED
+ perms = permissions.UpdateIssuePermissions(
+ permissions.COMMITTER_ACTIVE_PERMISSIONSET, project,
+ self.RESTRICTED_ISSUE3, {OWNER_ID})
+ self.assertNotIn('editissue', perms.perm_names)
+
def testUpdateIssuePermissions_CustomPermissionGrantsEditPermission(self):
project = project_pb2.Project()
project.committer_ids.append(999)
@@ -1650,6 +1685,39 @@
{111, 222}, permissions.PermissionSet([]), self.PROJECT,
[333]))
+ def testCanEditProjectConfig_Admin(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ project=fake.Project(project_id=789))
+ mr.perms = permissions.ADMIN_PERMISSIONSET
+ self.assertTrue(permissions.CanEditProjectConfig(mr, self.services))
+
+ def testCanEditProjectConfig_NormalUser(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ project=fake.Project(project_id=789))
+ mr.perms = permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET
+ self.assertFalse(permissions.CanEditProjectConfig(mr, self.services))
+
+ def testCanEditProjectConfig_Admin_FrozenConfig(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ project=fake.Project(project_id=789))
+ mr.perms = permissions.ADMIN_PERMISSIONSET
+ mr.auth.effective_ids = {567}
+
+ settings.config_freeze_override_users = {}
+ settings.config_freeze_project_ids = {789}
+ self.assertFalse(permissions.CanEditProjectConfig(mr, self.services))
+
+ def testCanEditProjectConfig_Admin_FrozenConfig_AllowedUser(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ project=fake.Project(project_id=789))
+ mr.perms = permissions.ADMIN_PERMISSIONSET
+ mr.auth.effective_ids = {567}
+
+ settings.config_freeze_override_users = {789: 'allowlisteduser@test.com'}
+ settings.config_freeze_project_ids = {789}
+
+ self.assertTrue(permissions.CanEditProjectConfig(mr, self.services))
+
def testCanViewComponentDef_ComponentAdmin(self):
cd = tracker_pb2.ComponentDef(admin_ids=[111])
perms = permissions.PermissionSet([])
@@ -1667,36 +1735,51 @@
{111}, permissions.PermissionSet([]),
None, cd))
- def testCanEditComponentDef_ComponentAdmin(self):
+ def testCanEditComponentDefLegacy_ComponentAdmin(self):
cd = tracker_pb2.ComponentDef(admin_ids=[111], path='Whole')
sub_cd = tracker_pb2.ComponentDef(admin_ids=[222], path='Whole>Part')
config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
config.component_defs.append(cd)
config.component_defs.append(sub_cd)
perms = permissions.PermissionSet([])
- self.assertTrue(permissions.CanEditComponentDef(
- {111}, perms, None, cd, config))
- self.assertFalse(permissions.CanEditComponentDef(
- {222}, perms, None, cd, config))
- self.assertFalse(permissions.CanEditComponentDef(
- {999}, perms, None, cd, config))
- self.assertTrue(permissions.CanEditComponentDef(
- {111}, perms, None, sub_cd, config))
- self.assertTrue(permissions.CanEditComponentDef(
- {222}, perms, None, sub_cd, config))
- self.assertFalse(permissions.CanEditComponentDef(
- {999}, perms, None, sub_cd, config))
+ self.assertTrue(
+ permissions.CanEditComponentDefLegacy({111}, perms, None, cd, config))
+ self.assertFalse(
+ permissions.CanEditComponentDefLegacy({222}, perms, None, cd, config))
+ self.assertFalse(
+ permissions.CanEditComponentDefLegacy({999}, perms, None, cd, config))
+ self.assertTrue(
+ permissions.CanEditComponentDefLegacy(
+ {111}, perms, None, sub_cd, config))
+ self.assertTrue(
+ permissions.CanEditComponentDefLegacy(
+ {222}, perms, None, sub_cd, config))
+ self.assertFalse(
+ permissions.CanEditComponentDefLegacy(
+ {999}, perms, None, sub_cd, config))
- def testCanEditComponentDef_ProjectOwners(self):
+ def testCanEditComponentDefLegacy_ProjectOwners(self):
cd = tracker_pb2.ComponentDef(path='Whole')
config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
config.component_defs.append(cd)
- self.assertTrue(permissions.CanEditComponentDef(
- {111}, permissions.PermissionSet([permissions.EDIT_PROJECT]),
- None, cd, config))
- self.assertFalse(permissions.CanEditComponentDef(
- {111}, permissions.PermissionSet([]),
- None, cd, config))
+ self.assertTrue(
+ permissions.CanEditComponentDefLegacy(
+ {111}, permissions.PermissionSet([permissions.EDIT_PROJECT]), None,
+ cd, config))
+ self.assertFalse(
+ permissions.CanEditComponentDefLegacy(
+ {111}, permissions.PermissionSet([]), None, cd, config))
+
+ def testCanEditComponentDefLegacy_FrozenProject(self):
+ cd = tracker_pb2.ComponentDef(path='Whole')
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ config.component_defs.append(cd)
+ project = project_pb2.Project(project_id=789)
+ settings.config_freeze_project_ids = {789}
+ self.assertFalse(
+ permissions.CanEditComponentDefLegacy(
+ {111}, permissions.PermissionSet([permissions.EDIT_PROJECT]),
+ project, cd, config))
def testCanViewFieldDef_FieldAdmin(self):
fd = tracker_pb2.FieldDef(admin_ids=[111])
diff --git a/framework/test/profiler_test.py b/framework/test/profiler_test.py
index 3cc7e85..3694e93 100644
--- a/framework/test/profiler_test.py
+++ b/framework/test/profiler_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Test for monorail.framework.profiler."""
from __future__ import print_function
@@ -73,7 +72,7 @@
with prof.Phase('baz'):
raise Exception('whoops')
except Exception as e:
- self.assertEqual(e.message, 'whoops')
+ self.assertEqual(str(e), 'whoops')
finally:
self.assertEqual(prof.current_phase.name, 'overall profile')
self.assertEqual(prof.top_phase.subphases[0].subphases[1].name, 'baz')
diff --git a/framework/test/ratelimiter_test.py b/framework/test/ratelimiter_test.py
index 84230e8..bfc8de7 100644
--- a/framework/test/ratelimiter_test.py
+++ b/framework/test/ratelimiter_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for RateLimiter.
"""
diff --git a/framework/test/reap_test.py b/framework/test/reap_test.py
index 92d17fb..e01273f 100644
--- a/framework/test/reap_test.py
+++ b/framework/test/reap_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for the reap module."""
from __future__ import print_function
@@ -20,7 +19,7 @@
from framework import reap
from framework import sql
-from proto import project_pb2
+from mrproto import project_pb2
from services import service_manager
from services import template_svc
from testing import fake
diff --git a/framework/test/registerpages_helpers_test.py b/framework/test/registerpages_helpers_test.py
deleted file mode 100644
index 61c489e..0000000
--- a/framework/test/registerpages_helpers_test.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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
-
-"""Tests for URL handler registration helper functions."""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-
-import unittest
-
-import webapp2
-
-from framework import registerpages_helpers
-
-
-class SendRedirectInScopeTest(unittest.TestCase):
-
- def testMakeRedirectInScope_Error(self):
- self.assertRaises(
- AssertionError,
- registerpages_helpers.MakeRedirectInScope, 'no/initial/slash', 'p')
- self.assertRaises(
- AssertionError,
- registerpages_helpers.MakeRedirectInScope, '', 'p')
-
- def testMakeRedirectInScope_Normal(self):
- factory = registerpages_helpers.MakeRedirectInScope('/', 'p')
- # Non-dasher, normal case
- request = webapp2.Request.blank(
- path='/p/foo', headers={'Host': 'example.com'})
- response = webapp2.Response()
- redirector = factory(request, response)
- redirector.get()
- self.assertEqual(response.location, '//example.com/p/foo/')
- self.assertEqual(response.status, '301 Moved Permanently')
-
- def testMakeRedirectInScope_Temporary(self):
- factory = registerpages_helpers.MakeRedirectInScope(
- '/', 'p', permanent=False)
- request = webapp2.Request.blank(
- path='/p/foo', headers={'Host': 'example.com'})
- response = webapp2.Response()
- redirector = factory(request, response)
- redirector.get()
- self.assertEqual(response.location, '//example.com/p/foo/')
- self.assertEqual(response.status, '302 Moved Temporarily')
-
- def testMakeRedirectInScope_KeepQueryString(self):
- factory = registerpages_helpers.MakeRedirectInScope(
- '/', 'p', keep_qs=True)
- request = webapp2.Request.blank(
- path='/p/foo?q=1', headers={'Host': 'example.com'})
- response = webapp2.Response()
- redirector = factory(request, response)
- redirector.get()
- self.assertEqual(response.location, '//example.com/p/foo/?q=1')
- self.assertEqual(response.status, '302 Moved Temporarily')
diff --git a/framework/test/servlet_helpers_test.py b/framework/test/servlet_helpers_test.py
index 870de40..4700e12 100644
--- a/framework/test/servlet_helpers_test.py
+++ b/framework/test/servlet_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for servlet base class helper functions."""
from __future__ import print_function
@@ -12,12 +11,12 @@
import settings
from google.appengine.ext import testbed
-
+from six.moves import urllib
from framework import permissions
from framework import servlet_helpers
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
from testing import testing_helpers
@@ -217,30 +216,21 @@
def tearDown(self):
self.testbed.deactivate()
- def testCreateLoginUrl(self):
- _, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/issues/detail?id=123&q=term', project=self.project)
- url = servlet_helpers.SafeCreateLoginURL(mr, 'current.url.to.return.to')
- # Ensure that users can pick their account to use with Monorail.
- self.assertIn('/AccountChooser', url)
- self.assertIn('current.url.to.return.to', url)
-
- def testCreateLoginUrl(self):
- _, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/issues/detail?id=123&q=term', project=self.project)
- url = servlet_helpers.SafeCreateLoginURL(mr, 'current.url.to.return.to')
- # Ensure that users can pick their account to use with Monorail.
- self.assertIn('/AccountChooser', url)
- self.assertIn('current.url.to.return.to', url)
-
def testCreateEscapedLoginUrlFromMR(self):
_, mr = testing_helpers.GetRequestObjects(
path='/p/proj/issues/detail?id=123&q=term', project=self.project)
mr.current_page_url_encoded = (
'https%3A%2F%2Fbugs.chromium.org'
- '%2Fp%2Fchromium%2Fissues%2Fentry')
+ '%2Fp%2Fchromium%2Fissues%2Fentry%3F'
+ 'template%3DBuild%2520Infrastructure%26'
+ 'labels%3DRestrict-View-Google%2CInfra-Troopers')
url = servlet_helpers.SafeCreateLoginURL(mr)
- self.assertIn('https%3A%2F%2Fbugs.chromium.org%2Fp', url)
+ double_encoded_query = (
+ 'https%253A%252F%252Fbugs.chromium.org'
+ '%252Fp%252Fchromium%252Fissues%252Fentry%253F'
+ 'template%253DBuild%252520Infrastructure%2526'
+ 'labels%253DRestrict-View-Google%252CInfra-Troopers')
+ self.assertIn(double_encoded_query, url)
def testCreateLogoutUrl(self):
_, mr = testing_helpers.GetRequestObjects(
diff --git a/framework/test/servlet_test.py b/framework/test/servlet_test.py
index 694e493..33f3644 100644
--- a/framework/test/servlet_test.py
+++ b/framework/test/servlet_test.py
@@ -1,8 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for servlet base class module."""
from __future__ import print_function
from __future__ import division
@@ -11,38 +9,30 @@
import time
import mock
import unittest
+import logging
-from google.appengine.api import app_identity
from google.appengine.ext import testbed
-import webapp2
-
-from framework import framework_constants, servlet_helpers
+from framework import framework_constants
from framework import servlet
+from framework import servlet_helpers
from framework import xsrf
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
from services import service_manager
from testing import fake
from testing import testing_helpers
-class TestableServlet(servlet.Servlet):
+class _TestableServlet(servlet.Servlet):
"""A tiny concrete subclass of abstract class Servlet."""
- def __init__(self, request, response, services=None, do_post_redirect=True):
- super(TestableServlet, self).__init__(request, response, services=services)
+ def __init__(self, services=None, do_post_redirect=True):
+ super(_TestableServlet, self).__init__(services=services)
self.do_post_redirect = do_post_redirect
self.seen_post_data = None
- def ProcessFormData(self, _mr, post_data):
- self.seen_post_data = post_data
- if self.do_post_redirect:
- return '/This/Is?The=Next#Page'
- else:
- self.response.write('sending raw data to browser')
-
class ServletTest(unittest.TestCase):
@@ -53,8 +43,7 @@
user=fake.UserService(),
usergroup=fake.UserGroupService())
services.user.TestAddUser('user@example.com', 111)
- self.page_class = TestableServlet(
- webapp2.Request.blank('/'), webapp2.Response(), services=services)
+ self.page_class = servlet.Servlet(services=services)
self.testbed = testbed.Testbed()
self.testbed.activate()
self.testbed.init_user_stub()
@@ -69,6 +58,30 @@
self.assertTrue(self.page_class._TEMPLATE_PATH.endswith('/templates/'))
self.assertEqual(None, self.page_class._PAGE_TEMPLATE)
+ @mock.patch('flask.abort')
+ def testCheckForMovedProject_NoRedirect(self, mock_abort):
+ project = fake.Project(
+ project_name='proj', state=project_pb2.ProjectState.LIVE)
+ request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj', project=project)
+ self.page_class._CheckForMovedProject(mr, request)
+ mock_abort.assert_not_called()
+
+ request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj/source/browse/p/adminAdvanced', project=project)
+ self.page_class._CheckForMovedProject(mr, request)
+ mock_abort.assert_not_called()
+
+ @mock.patch('flask.redirect')
+ def testCheckForMovedProject_Redirect(self, mock_redirect):
+ project = fake.Project(project_name='proj', moved_to='http://example.com')
+ request, mr = testing_helpers.GetRequestObjects(
+ path='/p/proj', project=project)
+ self.page_class.request_path = '/p/test'
+ self.page_class._CheckForMovedProject(mr, request)
+ mock_redirect.assert_called_once_with(
+ 'http://127.0.0.1/hosting/moved?project=proj', code=302)
+
def testGatherBaseData(self):
project = self.page_class.services.project.TestAddProject(
'testproj', state=project_pb2.ProjectState.LIVE)
@@ -89,284 +102,6 @@
self.assertTrue(
base_data['currentPageURLEncoded'].endswith('%2Fp%2Ftestproj%2Ffeeds'))
- def testFormHandlerURL(self):
- self.assertEqual('/edit.do', self.page_class._FormHandlerURL('/'))
- self.assertEqual(
- '/something/edit.do',
- self.page_class._FormHandlerURL('/something/'))
- self.assertEqual(
- '/something/edit.do',
- self.page_class._FormHandlerURL('/something/edit.do'))
- self.assertEqual(
- '/something/detail_ezt.do',
- self.page_class._FormHandlerURL('/something/detail_ezt'))
-
- def testProcessForm_BadToken(self):
- user_id = 111
- token = 'no soup for you'
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={
- 'yesterday': 'thursday',
- 'today': 'friday',
- 'token': token
- },
- user_info={'user_id': user_id},
- method='POST',
- )
- self.assertRaises(
- xsrf.TokenIncorrect, self.page_class._DoFormProcessing, request, mr)
- self.assertEqual(None, self.page_class.seen_post_data)
-
- def testProcessForm_XhrAllowed_BadToken(self):
- user_id = 111
- token = 'no soup for you'
-
- self.page_class.ALLOW_XHR = True
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={
- 'yesterday': 'thursday',
- 'today': 'friday',
- 'token': token
- },
- user_info={'user_id': user_id},
- method='POST',
- )
- self.assertRaises(
- xsrf.TokenIncorrect, self.page_class._DoFormProcessing, request, mr)
- self.assertEqual(None, self.page_class.seen_post_data)
-
- def testProcessForm_XhrAllowed_AcceptsPathToken(self):
- user_id = 111
- token = xsrf.GenerateToken(user_id, '/we/we/we')
-
- self.page_class.ALLOW_XHR = True
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={
- 'yesterday': 'thursday',
- 'today': 'friday',
- 'token': token
- },
- user_info={'user_id': user_id},
- method='POST',
- )
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._DoFormProcessing(request, mr)
- self.assertEqual(302, cm.exception.code) # forms redirect on success
-
- self.assertDictEqual(
- {
- 'yesterday': 'thursday',
- 'today': 'friday',
- 'token': token
- }, dict(self.page_class.seen_post_data))
-
- def testProcessForm_XhrAllowed_AcceptsXhrToken(self):
- user_id = 111
- token = xsrf.GenerateToken(user_id, 'xhr')
-
- self.page_class.ALLOW_XHR = True
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={'yesterday': 'thursday', 'today': 'friday', 'token': token},
- user_info={'user_id': user_id},
- method='POST',
- )
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._DoFormProcessing(request, mr)
- self.assertEqual(302, cm.exception.code) # forms redirect on success
-
- self.assertDictEqual(
- {
- 'yesterday': 'thursday',
- 'today': 'friday',
- 'token': token
- }, dict(self.page_class.seen_post_data))
-
- def testProcessForm_RawResponse(self):
- user_id = 111
- token = xsrf.GenerateToken(user_id, '/we/we/we')
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={'yesterday': 'thursday', 'today': 'friday', 'token': token},
- user_info={'user_id': user_id},
- method='POST',
- )
- self.page_class.do_post_redirect = False
- self.page_class._DoFormProcessing(request, mr)
- self.assertEqual(
- 'sending raw data to browser',
- self.page_class.response.body)
-
- def testProcessForm_Normal(self):
- user_id = 111
- token = xsrf.GenerateToken(user_id, '/we/we/we')
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/we/we/we?so=excited',
- params={'yesterday': 'thursday', 'today': 'friday', 'token': token},
- user_info={'user_id': user_id},
- method='POST',
- )
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._DoFormProcessing(request, mr)
- self.assertEqual(302, cm.exception.code) # forms redirect on success
-
- self.assertDictEqual(
- {'yesterday': 'thursday', 'today': 'friday', 'token': token},
- dict(self.page_class.seen_post_data))
-
- def testCalcProjectAlert(self):
- project = fake.Project(
- project_name='alerttest', state=project_pb2.ProjectState.LIVE)
-
- project_alert = servlet_helpers.CalcProjectAlert(project)
- self.assertEqual(project_alert, None)
-
- project.state = project_pb2.ProjectState.ARCHIVED
- project_alert = servlet_helpers.CalcProjectAlert(project)
- self.assertEqual(
- project_alert,
- 'Project is archived: read-only by members only.')
-
- delete_time = int(time.time() + framework_constants.SECS_PER_DAY * 1.5)
- project.delete_time = delete_time
- project_alert = servlet_helpers.CalcProjectAlert(project)
- self.assertEqual(project_alert, 'Scheduled for deletion in 1 day.')
-
- delete_time = int(time.time() + framework_constants.SECS_PER_DAY * 2.5)
- project.delete_time = delete_time
- project_alert = servlet_helpers.CalcProjectAlert(project)
- self.assertEqual(project_alert, 'Scheduled for deletion in 2 days.')
-
- def testCheckForMovedProject_NoRedirect(self):
- project = fake.Project(
- project_name='proj', state=project_pb2.ProjectState.LIVE)
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- self.page_class._CheckForMovedProject(mr, request)
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/source/browse/p/adminAdvanced', project=project)
- self.page_class._CheckForMovedProject(mr, request)
-
- def testCheckForMovedProject_Redirect(self):
- project = fake.Project(project_name='proj', moved_to='http://example.com')
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._CheckForMovedProject(mr, request)
- self.assertEqual(302, cm.exception.code) # redirect because project moved
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/source/browse/p/adminAdvanced', project=project)
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._CheckForMovedProject(mr, request)
- self.assertEqual(302, cm.exception.code) # redirect because project moved
-
- def testCheckForMovedProject_AdminAdvanced(self):
- """We do not redirect away from the page that edits project state."""
- project = fake.Project(project_name='proj', moved_to='http://example.com')
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/adminAdvanced', project=project)
- self.page_class._CheckForMovedProject(mr, request)
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/adminAdvanced?ts=123234', project=project)
- self.page_class._CheckForMovedProject(mr, request)
-
- request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj/adminAdvanced.do', project=project)
- self.page_class._CheckForMovedProject(mr, request)
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_RedirBrandedProject(self):
- """We redirect for a branded project if the user typed a different host."""
- project = fake.Project(project_name='proj')
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/proj/path', project=project)
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
- self.assertEqual(302, cm.exception.code) # forms redirect on success
- self.assertEqual('https://branded.example.com/p/proj/path?redir=1',
- cm.exception.location)
-
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/proj/path?query', project=project)
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
- self.assertEqual(302, cm.exception.code) # forms redirect on success
- self.assertEqual('https://branded.example.com/p/proj/path?query&redir=1',
- cm.exception.location)
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_AvoidRedirLoops(self):
- """Don't redirect for a branded project if already redirected."""
- project = fake.Project(project_name='proj')
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/proj/path?redir=1', project=project)
- # No redirect happens.
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_NonProjectPage(self):
- """Don't redirect for a branded project if not in any project."""
- request, _mr = testing_helpers.GetRequestObjects(
- path='/u/user@example.com')
- # No redirect happens.
- self.page_class._MaybeRedirectToBrandedDomain(request, None)
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_AlreadyOnBrandedHost(self):
- """Don't redirect for a branded project if already on branded domain."""
- project = fake.Project(project_name='proj')
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/proj/path', project=project)
- request.host = 'branded.example.com'
- # No redirect happens.
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_Localhost(self):
- """Don't redirect for a branded project on localhost."""
- project = fake.Project(project_name='proj')
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/proj/path', project=project)
- request.host = 'localhost:8080'
- # No redirect happens.
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
-
- request.host = '0.0.0.0:8080'
- # No redirect happens.
- self.page_class._MaybeRedirectToBrandedDomain(request, 'proj')
-
- @mock.patch('settings.branded_domains',
- {'proj': 'branded.example.com', '*': 'bugs.chromium.org'})
- def testMaybeRedirectToBrandedDomain_NotBranded(self):
- """Don't redirect for a non-branded project."""
- project = fake.Project(project_name='other')
- request, _mr = testing_helpers.GetRequestObjects(
- path='/p/other/path?query', project=project)
- request.host = 'branded.example.com' # But other project is unbranded.
-
- with self.assertRaises(webapp2.HTTPException) as cm:
- self.page_class._MaybeRedirectToBrandedDomain(request, 'other')
- self.assertEqual(302, cm.exception.code) # forms redirect on success
- self.assertEqual('https://bugs.chromium.org/p/other/path?query&redir=1',
- cm.exception.location)
-
def testGatherHelpData_Normal(self):
project = fake.Project(project_name='proj')
_request, mr = testing_helpers.GetRequestObjects(
@@ -374,102 +109,3 @@
help_data = self.page_class.GatherHelpData(mr, {})
self.assertEqual(None, help_data['cue'])
self.assertEqual(None, help_data['account_cue'])
-
- def testGatherHelpData_VacationReminder(self):
- project = fake.Project(project_name='proj')
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- mr.auth.user_id = 111
- mr.auth.user_pb.vacation_message = 'Gone skiing'
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual('you_are_on_vacation', help_data['cue'])
-
- self.page_class.services.user.SetUserPrefs(
- 'cnxn', 111,
- [user_pb2.UserPrefValue(name='you_are_on_vacation', value='true')])
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual(None, help_data['cue'])
- self.assertEqual(None, help_data['account_cue'])
-
- def testGatherHelpData_YouAreBouncing(self):
- project = fake.Project(project_name='proj')
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- mr.auth.user_id = 111
- mr.auth.user_pb.email_bounce_timestamp = 1497647529
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual('your_email_bounced', help_data['cue'])
-
- self.page_class.services.user.SetUserPrefs(
- 'cnxn', 111,
- [user_pb2.UserPrefValue(name='your_email_bounced', value='true')])
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual(None, help_data['cue'])
- self.assertEqual(None, help_data['account_cue'])
-
- def testGatherHelpData_ChildAccount(self):
- """Display a warning when user is signed in to a child account."""
- project = fake.Project(project_name='proj')
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/proj', project=project)
- mr.auth.user_pb.linked_parent_id = 111
- help_data = self.page_class.GatherHelpData(mr, {})
- self.assertEqual(None, help_data['cue'])
- self.assertEqual('switch_to_parent_account', help_data['account_cue'])
- self.assertEqual('user@example.com', help_data['parent_email'])
-
- def testGatherDebugData_Visibility(self):
- project = fake.Project(
- project_name='testtest', state=project_pb2.ProjectState.LIVE)
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/foo/servlet_path', project=project)
- debug_data = self.page_class.GatherDebugData(mr, {})
- self.assertEqual('off', debug_data['dbg'])
-
- _request, mr = testing_helpers.GetRequestObjects(
- path='/p/foo/servlet_path?debug=1', project=project)
- debug_data = self.page_class.GatherDebugData(mr, {})
- self.assertEqual('on', debug_data['dbg'])
-
-
-class ProjectIsRestrictedTest(unittest.TestCase):
-
- def testNonRestrictedProject(self):
- proj = project_pb2.Project()
- mr = testing_helpers.MakeMonorailRequest()
- mr.project = proj
-
- proj.access = project_pb2.ProjectAccess.ANYONE
- proj.state = project_pb2.ProjectState.LIVE
- self.assertFalse(servlet_helpers.ProjectIsRestricted(mr))
-
- proj.state = project_pb2.ProjectState.ARCHIVED
- self.assertFalse(servlet_helpers.ProjectIsRestricted(mr))
-
- def testRestrictedProject(self):
- proj = project_pb2.Project()
- mr = testing_helpers.MakeMonorailRequest()
- mr.project = proj
-
- proj.state = project_pb2.ProjectState.LIVE
- proj.access = project_pb2.ProjectAccess.MEMBERS_ONLY
- self.assertTrue(servlet_helpers.ProjectIsRestricted(mr))
-
-
-class VersionBaseTest(unittest.TestCase):
-
- @mock.patch('settings.local_mode', True)
- def testLocalhost(self):
- request = webapp2.Request.blank('/', base_url='http://localhost:8080')
- actual = servlet_helpers.VersionBaseURL(request)
- expected = 'http://localhost:8080'
- self.assertEqual(expected, actual)
-
- @mock.patch('settings.local_mode', False)
- @mock.patch('google.appengine.api.app_identity.get_default_version_hostname')
- def testProd(self, mock_gdvh):
- mock_gdvh.return_value = 'monorail-prod.appspot.com'
- request = webapp2.Request.blank('/', base_url='https://bugs.chromium.org')
- actual = servlet_helpers.VersionBaseURL(request)
- expected = 'https://test-dot-monorail-prod.appspot.com'
- self.assertEqual(expected, actual)
diff --git a/framework/test/sorting_test.py b/framework/test/sorting_test.py
index 4251308..44aa6f7 100644
--- a/framework/test/sorting_test.py
+++ b/framework/test/sorting_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for sorting.py functions."""
from __future__ import print_function
@@ -19,7 +18,7 @@
from framework import sorting
from framework import framework_views
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from testing import fake
from testing import testing_helpers
from tracker import tracker_bizobj
@@ -45,18 +44,18 @@
"""The point of DescendingValue is to reverse the sort order."""
anti_a = sorting.DescendingValue.MakeDescendingValue('a')
anti_b = sorting.DescendingValue.MakeDescendingValue('b')
- self.assertTrue(anti_a > anti_b)
+ self.assertGreater(anti_a, anti_b)
def testMaybeMakeDescending(self):
"""It returns an accessor that makes DescendingValue iff arg is True."""
asc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', False)
asc_value = asc_accessor('fake issue')
- self.assertTrue(asc_value is 'a')
+ self.assertEqual(asc_value, 'a')
desc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', True)
print(desc_accessor)
desc_value = desc_accessor('fake issue')
- self.assertTrue(isinstance(desc_value, sorting.DescendingValue))
+ self.assertIsInstance(desc_value, sorting.DescendingValue)
class SortingTest(unittest.TestCase):
@@ -161,9 +160,9 @@
accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
# Case 1: accessor generates no values.
- self.assertEqual(sorting.MAX_STRING, accessor(art))
+ self.assertEqual([sorting.MAX_STRING], accessor(art))
neg_accessor = MakeDescending(accessor)
- self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+ self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
# Case 2: A single well-known value
art.component_ids = [33]
@@ -186,9 +185,9 @@
accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
# Case 1: accessor generates no values.
- self.assertEqual(sorting.MAX_STRING, accessor(art))
+ self.assertEqual([sorting.MAX_STRING], accessor(art))
neg_accessor = MakeDescending(accessor)
- self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+ self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
# Case 2: A single oddball value
art.component_ids = [33]
@@ -212,9 +211,9 @@
# Case 1: accessor generates no values.
accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
- self.assertEqual(sorting.MAX_STRING, accessor(art))
+ self.assertEqual([sorting.MAX_STRING], accessor(art))
neg_accessor = MakeDescending(accessor)
- self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+ self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
# Case 2: A single well-known value
art.labels = ['Pri-Med']
diff --git a/framework/test/sql_test.py b/framework/test/sql_test.py
index f073e24..e8408cc 100644
--- a/framework/test/sql_test.py
+++ b/framework/test/sql_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for the sql module."""
from __future__ import print_function
diff --git a/framework/test/table_view_helpers_test.py b/framework/test/table_view_helpers_test.py
index 0260308..f0c6643 100644
--- a/framework/test/table_view_helpers_test.py
+++ b/framework/test/table_view_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for table_view_helpers classes and functions."""
from __future__ import print_function
@@ -9,12 +8,12 @@
from __future__ import absolute_import
import collections
+import six
import unittest
-import logging
from framework import framework_views
from framework import table_view_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
from testing import fake
from testing import testing_helpers
from tracker import tracker_bizobj
@@ -147,8 +146,9 @@
users_by_id=self.users_by_id)
self.assertEqual(cell.type, table_view_helpers.CELL_TYPE_ATTR)
self.assertEqual(len(cell.values), 2)
- self.assertItemsEqual([cell.values[0].item, cell.values[1].item],
- ['foo@example.com', 'f...@example.com'])
+ six.assertCountEqual(
+ self, [cell.values[0].item, cell.values[1].item],
+ ['foo@example.com', 'f...@example.com'])
# TODO(jrobbins): TableCellProject, TableCellStars
diff --git a/framework/test/template_helpers_test.py b/framework/test/template_helpers_test.py
index 85296fa..5f018e4 100644
--- a/framework/test/template_helpers_test.py
+++ b/framework/test/template_helpers_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unit tests for template_helpers module."""
@@ -82,7 +81,6 @@
# pylint: disable=anomalous-unicode-escape-in-string
test_data = (
u'This is a short string.',
-
u'This is a much longer string. '
u'This is a much longer string. '
u'This is a much longer string. '
@@ -95,52 +93,52 @@
u'This is a much longer string. ',
# This is a short escaped i18n string
- '\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab'.decode('utf-8'),
+ b'\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab'.decode('utf-8'),
# This is a longer i18n string
- '\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
- '\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
- '\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
- '\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
- '\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
- '\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
- '\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
- '\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '.decode('utf-8'),
+ b'\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
+ b'\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
+ b'\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
+ b'\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
+ b'\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
+ b'\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '
+ b'\xd5\xa1\xd5\xba\xd5\xa1\xd5\xaf\xd5\xab '
+ b'\xe6\x88\x91\xe8\x83\xbd\xe5\x90\x9e '.decode('utf-8'),
# This is a longer i18n string that was causing trouble.
- '\u041d\u0430 \u0431\u0435\u0440\u0435\u0433\u0443'
- ' \u043f\u0443\u0441\u0442\u044b\u043d\u043d\u044b\u0445'
- ' \u0432\u043e\u043b\u043d \u0421\u0442\u043e\u044f\u043b'
- ' \u043e\u043d, \u0434\u0443\u043c'
- ' \u0432\u0435\u043b\u0438\u043a\u0438\u0445'
- ' \u043f\u043e\u043b\u043d, \u0418'
- ' \u0432\u0434\u0430\u043b\u044c'
- ' \u0433\u043b\u044f\u0434\u0435\u043b.'
- ' \u041f\u0440\u0435\u0434 \u043d\u0438\u043c'
- ' \u0448\u0438\u0440\u043e\u043a\u043e'
- ' \u0420\u0435\u043a\u0430'
- ' \u043d\u0435\u0441\u043b\u0430\u0441\u044f;'
- ' \u0431\u0435\u0434\u043d\u044b\u0439'
- ' \u0447\u0451\u043b\u043d \u041f\u043e'
- ' \u043d\u0435\u0439'
- ' \u0441\u0442\u0440\u0435\u043c\u0438\u043b\u0441\u044f'
- ' \u043e\u0434\u0438\u043d\u043e\u043a\u043e.'
- ' \u041f\u043e \u043c\u0448\u0438\u0441\u0442\u044b\u043c,'
- ' \u0442\u043e\u043f\u043a\u0438\u043c'
- ' \u0431\u0435\u0440\u0435\u0433\u0430\u043c'
- ' \u0427\u0435\u0440\u043d\u0435\u043b\u0438'
- ' \u0438\u0437\u0431\u044b \u0437\u0434\u0435\u0441\u044c'
- ' \u0438 \u0442\u0430\u043c, \u041f\u0440\u0438\u044e\u0442'
- ' \u0443\u0431\u043e\u0433\u043e\u0433\u043e'
- ' \u0447\u0443\u0445\u043e\u043d\u0446\u0430;'
- ' \u0418 \u043b\u0435\u0441,'
- ' \u043d\u0435\u0432\u0435\u0434\u043e\u043c\u044b\u0439'
- ' \u043b\u0443\u0447\u0430\u043c \u0412'
- ' \u0442\u0443\u043c\u0430\u043d\u0435'
- ' \u0441\u043f\u0440\u044f\u0442\u0430\u043d\u043d\u043e'
- '\u0433\u043e \u0441\u043e\u043b\u043d\u0446\u0430,'
- ' \u041a\u0440\u0443\u0433\u043e\u043c'
- ' \u0448\u0443\u043c\u0435\u043b.'.decode('utf-8'))
+ u'\u041d\u0430 \u0431\u0435\u0440\u0435\u0433\u0443'
+ u' \u043f\u0443\u0441\u0442\u044b\u043d\u043d\u044b\u0445'
+ u' \u0432\u043e\u043b\u043d \u0421\u0442\u043e\u044f\u043b'
+ u' \u043e\u043d, \u0434\u0443\u043c'
+ u' \u0432\u0435\u043b\u0438\u043a\u0438\u0445'
+ u' \u043f\u043e\u043b\u043d, \u0418'
+ u' \u0432\u0434\u0430\u043b\u044c'
+ u' \u0433\u043b\u044f\u0434\u0435\u043b.'
+ u' \u041f\u0440\u0435\u0434 \u043d\u0438\u043c'
+ u' \u0448\u0438\u0440\u043e\u043a\u043e'
+ u' \u0420\u0435\u043a\u0430'
+ u' \u043d\u0435\u0441\u043b\u0430\u0441\u044f;'
+ u' \u0431\u0435\u0434\u043d\u044b\u0439'
+ u' \u0447\u0451\u043b\u043d \u041f\u043e'
+ u' \u043d\u0435\u0439'
+ u' \u0441\u0442\u0440\u0435\u043c\u0438\u043b\u0441\u044f'
+ u' \u043e\u0434\u0438\u043d\u043e\u043a\u043e.'
+ u' \u041f\u043e \u043c\u0448\u0438\u0441\u0442\u044b\u043c,'
+ u' \u0442\u043e\u043f\u043a\u0438\u043c'
+ u' \u0431\u0435\u0440\u0435\u0433\u0430\u043c'
+ u' \u0427\u0435\u0440\u043d\u0435\u043b\u0438'
+ u' \u0438\u0437\u0431\u044b \u0437\u0434\u0435\u0441\u044c'
+ u' \u0438 \u0442\u0430\u043c, \u041f\u0440\u0438\u044e\u0442'
+ u' \u0443\u0431\u043e\u0433\u043e\u0433\u043e'
+ u' \u0447\u0443\u0445\u043e\u043d\u0446\u0430;'
+ u' \u0418 \u043b\u0435\u0441,'
+ u' \u043d\u0435\u0432\u0435\u0434\u043e\u043c\u044b\u0439'
+ u' \u043b\u0443\u0447\u0430\u043c \u0412'
+ u' \u0442\u0443\u043c\u0430\u043d\u0435'
+ u' \u0441\u043f\u0440\u044f\u0442\u0430\u043d\u043d\u043e'
+ u'\u0433\u043e \u0441\u043e\u043b\u043d\u0446\u0430,'
+ u' \u041a\u0440\u0443\u0433\u043e\u043c'
+ u' \u0448\u0443\u043c\u0435\u043b.')
for unicode_s in test_data:
# Get the length in characters, not bytes.
diff --git a/framework/test/timestr_test.py b/framework/test/timestr_test.py
index ad11249..e4dd907 100644
--- a/framework/test/timestr_test.py
+++ b/framework/test/timestr_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Unittest for timestr module."""
from __future__ import print_function
diff --git a/framework/test/ts_mon_js_test.py b/framework/test/ts_mon_js_test.py
index 4231b76..da48378 100644
--- a/framework/test/ts_mon_js_test.py
+++ b/framework/test/ts_mon_js_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 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 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for MonorailTSMonJSHandler."""
from __future__ import print_function
@@ -13,10 +12,8 @@
from mock import patch
import flask
-import webapp2
from google.appengine.ext import testbed
-from framework.ts_mon_js import FlaskMonorailTSMonJSHandler
from framework.ts_mon_js import MonorailTSMonJSHandler
from services import service_manager
@@ -27,66 +24,12 @@
self.testbed = testbed.Testbed()
self.testbed.activate()
self.testbed.init_user_stub()
-
- def tearDown(self):
- self.testbed.deactivate()
-
- @patch('framework.xsrf.ValidateToken')
- @patch('time.time')
- def testSubmitMetrics(self, _mockTime, _mockValidateToken):
- """Test normal case POSTing metrics."""
- _mockTime.return_value = 1537821859
- req = webapp2.Request.blank('/_/ts_mon_js')
- req.body = json.dumps({
- 'metrics': [{
- 'MetricInfo': {
- 'Name': 'monorail/frontend/issue_update_latency',
- 'ValueType': 2,
- },
- 'Cells': [{
- 'value': {
- 'sum': 1234,
- 'count': 4321,
- 'buckets': {
- 0: 123,
- 1: 321,
- 2: 213,
- },
- },
- 'fields': {
- 'client_id': '789',
- 'host_name': 'rutabaga',
- 'document_visible': True,
- },
- 'start_time': 1537821859 - 60,
- }],
- }],
- })
- res = webapp2.Response()
- ts_mon_handler = MonorailTSMonJSHandler(request=req, response=res)
- class MockApp(object):
- def __init__(self):
- self.config = {'services': service_manager.Services()}
- ts_mon_handler.app = MockApp()
-
- ts_mon_handler.post()
-
- self.assertEqual(res.status_int, 201)
- self.assertEqual(res.body, 'Ok.')
-
-
-class FlaskMonorailTSMonJSHandlerTest(unittest.TestCase):
-
- def setUp(self):
- self.testbed = testbed.Testbed()
- self.testbed.activate()
- self.testbed.init_user_stub()
self.services = service_manager.Services()
self.app = flask.Flask('test_app')
self.app.config['TESTING'] = True
self.app.add_url_rule(
'/_/ts_mon_js.do',
- view_func=FlaskMonorailTSMonJSHandler(
+ view_func=MonorailTSMonJSHandler(
services=self.services).PostMonorailTSMonJSHandler,
methods=['POST'])
@@ -95,7 +38,7 @@
@patch('framework.xsrf.ValidateToken')
@patch('time.time')
- def testFlaskSubmitMetrics(self, _mockTime, _mockValidateToken):
+ def testSubmitMetrics(self, _mockTime, _mockValidateToken):
"""Test normal case POSTing metrics."""
_mockTime.return_value = 1537821859
res = self.app.test_client().post(
diff --git a/framework/test/validate_test.py b/framework/test/validate_test.py
index 9ea17fe..9716122 100644
--- a/framework/test/validate_test.py
+++ b/framework/test/validate_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""This file provides unit tests for Validate functions."""
from __future__ import print_function
@@ -31,6 +30,11 @@
'user@e-x-a-m-p-l-e.com',
'user@e-x.am-ple.com',
'user@e--xample.com',
+ 'user@example.c',
+ 'user@example.comcomcomc',
+ 'user@example.co-m',
+ 'user@example.c0m',
+ 'very-long-email-address@very-long-domain.iam.abcdefghijklmnopqrstuvwxyz',
]
BAD_EMAIL_ADDRESSES = [
@@ -52,12 +56,8 @@
'user@example-.com',
'user@example',
'user@example.',
- 'user@example.c',
- 'user@example.comcomcomc',
- 'user@example.co-m',
'user@exa_mple.com',
'user@exa-_mple.com',
- 'user@example.c0m',
]
def testIsValidEmail(self):
diff --git a/framework/test/warmup_test.py b/framework/test/warmup_test.py
index 13223f1..b3eab65 100644
--- a/framework/test/warmup_test.py
+++ b/framework/test/warmup_test.py
@@ -1,7 +1,6 @@
-# Copyright 2017 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 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for the warmup servlet."""
from __future__ import print_function
@@ -25,7 +24,7 @@
response = client.get('/')
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.data, '')
+ self.assertEqual(response.data, b'')
def testHandleStart(self):
app = flask.Flask(__name__)
@@ -35,7 +34,7 @@
response = client.get('/')
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.data, '')
+ self.assertEqual(response.data, b'')
def testHandleStop(self):
app = flask.Flask(__name__)
@@ -45,4 +44,4 @@
response = client.get('/')
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.data, '')
+ self.assertEqual(response.data, b'')
diff --git a/framework/test/xsrf_test.py b/framework/test/xsrf_test.py
index aa04570..1039ab6 100644
--- a/framework/test/xsrf_test.py
+++ b/framework/test/xsrf_test.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Tests for XSRF utility functions."""
from __future__ import print_function
diff --git a/framework/timestr.py b/framework/timestr.py
index 2b32e8c..b768c73 100644
--- a/framework/timestr.py
+++ b/framework/timestr.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Time-to-string and time-from-string routines."""
from __future__ import print_function
diff --git a/framework/trimvisitedpages.py b/framework/trimvisitedpages.py
index f43ab09..039131f 100644
--- a/framework/trimvisitedpages.py
+++ b/framework/trimvisitedpages.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Classes to handle cron requests to trim users' hotlists/issues visited."""
from __future__ import print_function
@@ -11,7 +10,7 @@
from framework import jsonfeed
-class TrimVisitedPages(jsonfeed.FlaskInternalTask):
+class TrimVisitedPages(jsonfeed.InternalTask):
"""Look for users with more than 10 visited hotlists and deletes extras."""
diff --git a/framework/ts_mon_js.py b/framework/ts_mon_js.py
index 0dbb121..2db9f30 100644
--- a/framework/ts_mon_js.py
+++ b/framework/ts_mon_js.py
@@ -1,7 +1,6 @@
-# Copyright 2018 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 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""ts_mon JavaScript proxy handler."""
from __future__ import print_function
@@ -12,7 +11,6 @@
from framework import sql
from framework import xsrf
-from gae_ts_mon.handlers import TSMonJSHandler
from gae_ts_mon.flask_handlers import TSMonJSFlaskHandler
from google.appengine.api import users
@@ -81,41 +79,10 @@
units=ts_mon.MetricsDataUnits.MILLISECONDS)
-class MonorailTSMonJSHandler(TSMonJSHandler):
-
- def __init__(self, request=None, response=None):
- super(MonorailTSMonJSHandler, self).__init__(request, response)
- self.register_metrics([
- ISSUE_CREATE_LATENCY_METRIC,
- ISSUE_UPDATE_LATENCY_METRIC,
- AUTOCOMPLETE_POPULATE_LATENCY_METRIC,
- CHARTS_SWITCH_DATE_RANGE_METRIC,
- ISSUE_COMMENTS_LOAD_LATENCY_METRIC,
- DOM_CONTENT_LOADED_METRIC,
- ISSUE_LIST_LOAD_LATENCY_METRIC])
-
- def xsrf_is_valid(self, body):
- """This method expects the body dictionary to include two fields:
- `token` and `user_id`.
- """
- cnxn = sql.MonorailConnection()
- token = body.get('token')
- user = users.get_current_user()
- email = user.email() if user else None
-
- services = self.app.config.get('services')
- auth = authdata.AuthData.FromEmail(cnxn, email, services, autocreate=False)
- try:
- xsrf.ValidateToken(token, auth.user_id, xsrf.XHR_SERVLET_PATH)
- return True
- except xsrf.TokenIncorrect:
- return False
-
-
-class FlaskMonorailTSMonJSHandler(TSMonJSFlaskHandler):
+class MonorailTSMonJSHandler(TSMonJSFlaskHandler):
def __init__(self, services=None):
- super(FlaskMonorailTSMonJSHandler, self).__init__(
+ super(MonorailTSMonJSHandler, self).__init__(
flask=flask, services=services)
self.register_metrics(
[
diff --git a/framework/urls.py b/framework/urls.py
index f8a4a4d..0020b62 100644
--- a/framework/urls.py
+++ b/framework/urls.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Constants that define the Monorail URL space."""
from __future__ import print_function
diff --git a/framework/validate.py b/framework/validate.py
index ee26396..baf6d6e 100644
--- a/framework/validate.py
+++ b/framework/validate.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A set of Python input field validators."""
from __future__ import print_function
@@ -10,72 +9,28 @@
import re
-# RFC 2821-compliant email address regex
-#
-# Please see sections "4.1.2 Command Argument Syntax" and
-# "4.1.3 Address Literals" of: http://www.faqs.org/rfcs/rfc2821.html
-#
-# The following implementation is still a subset of RFC 2821. Fully
-# double-quoted <user> parts are not supported (since the RFC discourages
-# their use anyway), and using the backslash to escape other characters
-# that are normally invalid, such as commas, is not supported.
-#
-# The groups in this regular expression are:
-#
-# <user>: all of the valid non-quoted portion of the email address before
-# the @ sign (not including the @ sign)
-#
-# <domain>: all of the domain name between the @ sign (but not including it)
-# and the dot before the TLD (but not including that final dot)
-#
-# <tld>: the top-level domain after the last dot (but not including that
-# final dot)
-#
-_RFC_2821_EMAIL_REGEX = r"""(?x)
- (?P<user>
- # Part of the username that comes before any dots that may occur in it.
- # At least one of the listed non-dot characters is required before the
- # first dot.
- [-a-zA-Z0-9!#$%&'*+/=?^_`{|}~]+
-
- # Remaining part of the username that starts with the dot and
- # which may have other dots, if such a part exists. Only one dot
- # is permitted between each "Atom", and a trailing dot is not permitted.
- (?:[.][-a-zA-Z0-9!#$%&'*+/=?^_`{|}~]+)*
- )
-
- # Domain name, where subdomains are allowed. Also, dashes are allowed
- # given that they are preceded and followed by at least one character.
- @(?P<domain>
- (?:[0-9a-zA-Z] # at least one non-dash
- (?:[-]* # plus zero or more dashes
- [0-9a-zA-Z]+ # plus at least one non-dash
- )* # zero or more of dashes followed by non-dashes
- ) # one required domain part (may be a sub-domain)
-
- (?:\. # dot separator before additional sub-domain part
- [0-9a-zA-Z] # at least one non-dash
- (?:[-]* # plus zero or more dashes
- [0-9a-zA-Z]+ # plus at least one non-dash
- )* # zero or more of dashes followed by non-dashes
- )* # at least one sub-domain part and a dot
- )
- \. # dot separator before TLD
-
- # TLD, the part after 'usernames@domain.' which can consist of 2-9
- # letters.
- (?P<tld>[a-zA-Z]{2,9})
+# RFC 5322-compliant email address regex
+# https://stackoverflow.com/a/201378
+_RFC_2821_EMAIL_REGEX = r"""
+ (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|
+ "(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|
+ \\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@
+ (?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|
+ \[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|
+ [0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:
+ (?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|
+ \\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
"""
# object used with <re>.search() or <re>.sub() to find email addresses
# within a string (or with <re>.match() to find email addresses at the
# beginning of a string that may be followed by trailing characters,
# since <re>.match() implicitly anchors at the beginning of the string)
-RE_EMAIL_SEARCH = re.compile(_RFC_2821_EMAIL_REGEX)
+RE_EMAIL_SEARCH = re.compile(_RFC_2821_EMAIL_REGEX, re.X)
# object used with <re>.match to find strings that contain *only* a single
# email address (by adding the end-of-string anchor $)
-RE_EMAIL_ONLY = re.compile('^%s$' % _RFC_2821_EMAIL_REGEX)
+RE_EMAIL_ONLY = re.compile('^%s$' % _RFC_2821_EMAIL_REGEX, re.X)
_SCHEME_PATTERN = r'(?:https?|ftp)://'
_SHORT_HOST_PATTERN = (
diff --git a/framework/warmup.py b/framework/warmup.py
index a133107..d597fd3 100644
--- a/framework/warmup.py
+++ b/framework/warmup.py
@@ -1,7 +1,6 @@
-# Copyright 2017 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 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""A class to handle the initial warmup request from AppEngine."""
from __future__ import print_function
diff --git a/framework/xsrf.py b/framework/xsrf.py
index 75581ef..bc5ae33 100644
--- a/framework/xsrf.py
+++ b/framework/xsrf.py
@@ -1,7 +1,6 @@
-# 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 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Utility routines for avoiding cross-site-request-forgery."""
from __future__ import print_function
@@ -9,12 +8,12 @@
from __future__ import absolute_import
import base64
+import hashlib
import hmac
-import logging
+import six
import time
# This is a file in the top-level directory that you must edit before deploying
-import settings
from framework import framework_constants
from services import secrets_svc
@@ -43,7 +42,7 @@
XHR_SERVLET_PATH = 'xhr'
-DELIMITER = ':'
+DELIMITER = b':'
def GenerateToken(user_id, servlet_path, token_time=None):
@@ -63,16 +62,16 @@
ValueError: if the XSRF secret was not configured.
"""
token_time = token_time or int(time.time())
- digester = hmac.new(secrets_svc.GetXSRFKey())
- digester.update(str(user_id))
+ digester = hmac.new(secrets_svc.GetXSRFKey(), digestmod=hashlib.md5)
+ digester.update(six.ensure_binary(str(user_id)))
digester.update(DELIMITER)
- digester.update(servlet_path)
+ digester.update(six.ensure_binary(servlet_path))
digester.update(DELIMITER)
- digester.update(str(token_time))
+ digester.update(six.ensure_binary(str(token_time)))
digest = digester.digest()
- token = base64.urlsafe_b64encode('%s%s%d' % (digest, DELIMITER, token_time))
- return token
+ token = base64.urlsafe_b64encode(b'%s%s%d' % (digest, DELIMITER, token_time))
+ return six.ensure_str(token)
def ValidateToken(
@@ -91,7 +90,7 @@
raise TokenIncorrect('missing token')
try:
- decoded = base64.urlsafe_b64decode(str(token))
+ decoded = base64.urlsafe_b64decode(six.ensure_binary(token))
token_time = int(decoded.split(DELIMITER)[-1])
except (TypeError, ValueError):
raise TokenIncorrect('could not decode token')
@@ -104,8 +103,14 @@
# Perform constant time comparison to avoid timing attacks
different = 0
+ # In Python 3, zip(bytes, bytes) gives ints, but in Python 2,
+ # zip(str, str) gives strs. We need to call ord() in Python 2 only.
+ if isinstance(token, six.string_types):
+ token = list(map(ord, token))
+ if isinstance(expected_token, six.string_types):
+ expected_token = list(map(ord, expected_token))
for x, y in zip(token, expected_token):
- different |= ord(x) ^ ord(y)
+ different |= x ^ y
if different:
raise TokenIncorrect(
'presented token does not match expected token: %r != %r' % (