blob: 89fe587828f7d028a8b9d78cc7437a01d9394966 [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
15import 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
178def ComputeIssueEntryURL(mr, config):
179 """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ínezac4a6442022-05-15 19:05:13 +0200190 # TODO: remove the custom_issue_entry_url since its no longer
Copybara854996b2021-09-07 19:36:02 +0000191 if not config.custom_issue_entry_url:
192 return '/p/%s/issues/entry' % (mr.project_name)
193
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200194 return '/p/chromium/issues/wizard'
Copybara854996b2021-09-07 19:36:02 +0000195
196
197def IssueListURL(mr, config, query_string=None):
198 """Make an issue list URL for non-members or members."""
199 url = '/p/%s%s' % (mr.project_name, urls.ISSUE_LIST)
200 if query_string:
201 url += '?' + query_string
202 elif framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids):
203 if config and config.member_default_query:
204 url += '?q=' + urllib.quote_plus(config.member_default_query)
205 return url
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200206
207
208def ProjectIsRestricted(mr):
209 """Return True if the mr has a 'private' project."""
210 return (mr.project and mr.project.access != project_pb2.ProjectAccess.ANYONE)
211
212
213def SafeCreateLoginURL(mr, continue_url=None):
214 """Make a login URL w/ a detailed continue URL, otherwise use a short one."""
215 continue_url = continue_url or mr.current_page_url
216 try:
217 url = users.create_login_url(continue_url)
218 except users.RedirectTooLongError:
219 if mr.project_name:
220 url = users.create_login_url('/p/%s' % mr.project_name)
221 else:
222 url = users.create_login_url('/')
223
224 # Give the user a choice of existing accounts in their session
225 # or the option to add an account, even if they are currently
226 # signed in to exactly one account.
227 if mr.auth.user_id:
228 # Notice: this makes assuptions about the output of users.create_login_url,
229 # which can change at any time. See https://crbug.com/monorail/3352.
230 url = url.replace('/ServiceLogin', '/AccountChooser', 1)
231 return url
232
233
234def SafeCreateLogoutURL(mr):
235 """Make a logout URL w/ a detailed continue URL, otherwise use a short one."""
236 try:
237 return users.create_logout_url(mr.current_page_url)
238 except users.RedirectTooLongError:
239 if mr.project_name:
240 return users.create_logout_url('/p/%s' % mr.project_name)
241 else:
242 return users.create_logout_url('/')
243
244
245def VersionBaseURL(request):
246 """Return a version-specific URL that we use to load static assets."""
247 if settings.local_mode:
248 version_base = '%s://%s' % (request.scheme, request.host)
249 else:
250 version_base = '%s://%s-dot-%s' % (
251 request.scheme, modules.get_current_version_name(),
252 app_identity.get_default_version_hostname())
253
254 return version_base
255
256
257def CalcProjectAlert(project):
258 """Return a string to be shown as red text explaining the project state."""
259
260 project_alert = None
261
262 if project.read_only_reason:
263 project_alert = 'READ-ONLY: %s.' % project.read_only_reason
264 if project.moved_to:
265 project_alert = 'This project has moved to: %s.' % project.moved_to
266 elif project.delete_time:
267 delay_seconds = project.delete_time - time.time()
268 delay_days = delay_seconds // framework_constants.SECS_PER_DAY
269 if delay_days <= 0:
270 project_alert = 'Scheduled for deletion today.'
271 else:
272 days_word = 'day' if delay_days == 1 else 'days'
273 project_alert = (
274 'Scheduled for deletion in %d %s.' % (delay_days, days_word))
275 elif project.state == project_pb2.ProjectState.ARCHIVED:
276 project_alert = 'Project is archived: read-only by members only.'
277
278 return project_alert