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