blob: 4d4259caec36eea168ee860dfec29873434fbb4a [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Helper functions used by the Monorail servlet base class."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020010import settings
Copybara854996b2021-09-07 19:36:02 +000011import calendar
12import datetime
13import logging
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020014from six.moves import urllib
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020015import time
Copybara854996b2021-09-07 19:36:02 +000016
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020017from framework import framework_constants
Copybara854996b2021-09-07 19:36:02 +000018from framework import framework_bizobj
19from framework import framework_helpers
20from framework import permissions
21from framework import template_helpers
22from framework import urls
23from framework import xsrf
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010024from mrproto import project_pb2
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020025
26from google.appengine.api import app_identity
27from google.appengine.api import modules
28from google.appengine.api import users
Copybara854996b2021-09-07 19:36:02 +000029
30_ZERO = datetime.timedelta(0)
31
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020032
33class MethodNotSupportedError(NotImplementedError):
34 """An exception class for indicating that the method is not supported.
35
36 Used by GatherPageData and ProcessFormData in Servlet.
37 """
38 pass
39
40
41class _ContextDebugItem(object):
42 """Wrapper class to generate on-screen debugging output."""
43
44 def __init__(self, key, val):
45 """Store the key and generate a string for the value."""
46 self.key = key
47 if isinstance(val, list):
48 nested_debug_strs = [self.StringRep(v) for v in val]
49 self.val = '[%s]' % ', '.join(nested_debug_strs)
50 else:
51 self.val = self.StringRep(val)
52
53 def StringRep(self, val):
54 """Make a useful string representation of the given value."""
55 try:
56 return val.DebugString()
57 except Exception:
58 try:
59 return str(val.__dict__)
60 except Exception:
61 return repr(val)
62
63
64class ContextDebugCollection(object):
65 """Attach a title to a dictionary for exporting as a table of debug info."""
66
67 def __init__(self, title, collection):
68 self.title = title
69 self.collection = [
70 _ContextDebugItem(key, collection[key])
71 for key in sorted(collection.keys())
72 ]
73
74
Copybara854996b2021-09-07 19:36:02 +000075class _UTCTimeZone(datetime.tzinfo):
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +020076 """UTC"""
77
78 def utcoffset(self, _dt):
79 return _ZERO
80
81 def tzname(self, _dt):
82 return "UTC"
83
84 def dst(self, _dt):
85 return _ZERO
86
Copybara854996b2021-09-07 19:36:02 +000087
88_UTC = _UTCTimeZone()
89
90
91def GetBannerTime(timestamp):
92 """Converts a timestamp into EZT-ready data so it can appear in the banner.
93
94 Args:
95 timestamp: timestamp expressed in the following format:
96 [year,month,day,hour,minute,second]
97 e.g. [2009,3,20,21,45,50] represents March 20 2009 9:45:50 PM
98
99 Returns:
100 EZT-ready data used to display the time inside the banner message.
101 """
102 if timestamp is None:
103 return None
104
105 ts = datetime.datetime(*timestamp, tzinfo=_UTC)
106 return calendar.timegm(ts.timetuple())
107
108
109def AssertBasePermissionForUser(user, user_view):
110 """Verify user permissions and state.
111
112 Args:
113 user: user_pb2.User protocol buffer for the user
114 user_view: framework.views.UserView for the user
115 """
116 if permissions.IsBanned(user, user_view):
117 raise permissions.BannedUserException(
118 'You have been banned from using this site')
119
120
121def AssertBasePermission(mr):
122 """Make sure that the logged in user can view the requested page.
123
124 Args:
125 mr: common information parsed from the HTTP request.
126
127 Returns:
128 Nothing
129
130 Raises:
131 BannedUserException: If the user is banned.
132 PermissionException: If the user does not have permisssion to view.
133 """
134 AssertBasePermissionForUser(mr.auth.user_pb, mr.auth.user_view)
135
136 if mr.project_name and not CheckPerm(mr, permissions.VIEW):
137 logging.info('your perms are %r', mr.perms)
138 raise permissions.PermissionException(
139 'User is not allowed to view this project')
140
141
142def CheckPerm(mr, perm, art=None, granted_perms=None):
143 """Convenience method that makes permission checks easier.
144
145 Args:
146 mr: common information parsed from the HTTP request.
147 perm: A permission constant, defined in module framework.permissions
148 art: Optional artifact pb
149 granted_perms: optional set of perms granted specifically in that artifact.
150
151 Returns:
152 A boolean, whether the request can be satisfied, given the permission.
153 """
154 return mr.perms.CanUsePerm(
155 perm, mr.auth.effective_ids, mr.project,
156 permissions.GetRestrictions(art), granted_perms=granted_perms)
157
158
159def CheckPermForProject(mr, perm, project, art=None):
160 """Convenience method that makes permission checks for projects easier.
161
162 Args:
163 mr: common information parsed from the HTTP request.
164 perm: A permission constant, defined in module framework.permissions
165 project: The project to enforce permissions for.
166 art: Optional artifact pb
167
168 Returns:
169 A boolean, whether the request can be satisfied, given the permission.
170 """
171 perms = permissions.GetPermissions(
172 mr.auth.user_pb, mr.auth.effective_ids, project)
173 return perms.CanUsePerm(
174 perm, mr.auth.effective_ids, project, permissions.GetRestrictions(art))
175
176
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200177def ComputeIssueEntryURL(mr):
Copybara854996b2021-09-07 19:36:02 +0000178 """Compute the URL to use for the "New issue" subtab.
179
180 Args:
181 mr: commonly used info parsed from the request.
182 config: ProjectIssueConfig for the current project.
183
184 Returns:
185 A URL string to use. It will be simply "entry" in the non-customized
186 case. Otherewise it will be a fully qualified URL that includes some
187 query string parameters.
188 """
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200189 isMember = framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids)
190 if mr.project_name == 'chromium' and not isMember:
191 return '/p/chromium/issues/wizard'
192 else:
Copybara854996b2021-09-07 19:36:02 +0000193 return '/p/%s/issues/entry' % (mr.project_name)
194
Copybara854996b2021-09-07 19:36:02 +0000195def IssueListURL(mr, config, query_string=None):
196 """Make an issue list URL for non-members or members."""
197 url = '/p/%s%s' % (mr.project_name, urls.ISSUE_LIST)
198 if query_string:
199 url += '?' + query_string
200 elif framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids):
201 if config and config.member_default_query:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200202 url += '?q=' + urllib.parse.quote_plus(config.member_default_query)
Copybara854996b2021-09-07 19:36:02 +0000203 return url
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200204
205
206def ProjectIsRestricted(mr):
207 """Return True if the mr has a 'private' project."""
208 return (mr.project and mr.project.access != project_pb2.ProjectAccess.ANYONE)
209
210
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100211def SafeCreateLoginURL(mr):
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200212 """Make a login URL w/ a detailed continue URL, otherwise use a short one."""
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100213 current_url = mr.current_page_url_encoded
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200214 if settings.local_mode:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100215 current_url = mr.current_page_url
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200216 try:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200217 # Check the URL length
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100218 generated_login_url = users.create_login_url(current_url)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200219 except users.RedirectTooLongError:
220 if mr.project_name:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100221 current_url = '/p/%s' % mr.project_name
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200222 else:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100223 current_url = '/'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200224 if settings.local_mode:
225 return generated_login_url
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100226
227 current_parts = urllib.parse.urlparse(current_url)
228 current_query = current_parts.query
229 # Double encode only the query so that it survives redirect parsing.
230 current_url = urllib.parse.urlunparse(
231 current_parts[:3] + ('', urllib.parse.quote_plus(current_query), ''))
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200232 # URL to allow user to choose an account when >1 account is logged in.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100233 second_redirect_url = 'https://uc.appengine.google.com/_ah/conflogin?'
234 second_redirect_query = 'continue=' + current_url
235 second_redirect_uri = second_redirect_url + second_redirect_query
236
237 first_redirect_url = 'https://accounts.google.com/AccountChooser?'
238 first_redirect_params = {'continue': second_redirect_uri}
239 first_redirect_uri = first_redirect_url + urllib.parse.urlencode(
240 first_redirect_params)
241 return first_redirect_uri
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200242
243
244def SafeCreateLogoutURL(mr):
245 """Make a logout URL w/ a detailed continue URL, otherwise use a short one."""
246 try:
247 return users.create_logout_url(mr.current_page_url)
248 except users.RedirectTooLongError:
249 if mr.project_name:
250 return users.create_logout_url('/p/%s' % mr.project_name)
251 else:
252 return users.create_logout_url('/')
253
254
255def VersionBaseURL(request):
256 """Return a version-specific URL that we use to load static assets."""
257 if settings.local_mode:
258 version_base = '%s://%s' % (request.scheme, request.host)
259 else:
260 version_base = '%s://%s-dot-%s' % (
261 request.scheme, modules.get_current_version_name(),
262 app_identity.get_default_version_hostname())
263
264 return version_base
265
266
267def CalcProjectAlert(project):
268 """Return a string to be shown as red text explaining the project state."""
269
270 project_alert = None
271
272 if project.read_only_reason:
273 project_alert = 'READ-ONLY: %s.' % project.read_only_reason
274 if project.moved_to:
275 project_alert = 'This project has moved to: %s.' % project.moved_to
276 elif project.delete_time:
277 delay_seconds = project.delete_time - time.time()
278 delay_days = delay_seconds // framework_constants.SECS_PER_DAY
279 if delay_days <= 0:
280 project_alert = 'Scheduled for deletion today.'
281 else:
282 days_word = 'day' if delay_days == 1 else 'days'
283 project_alert = (
284 'Scheduled for deletion in %d %s.' % (delay_days, days_word))
285 elif project.state == project_pb2.ProjectState.ARCHIVED:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100286 project_alert = 'Project is archived and read-only.'
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200287
288 return project_alert