OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered 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 and classes used throughout Monorail.""" |
| 7 |
| 8 import logging |
| 9 import random |
| 10 import string |
| 11 import textwrap |
| 12 import threading |
| 13 import time |
| 14 import traceback |
| 15 import urllib |
| 16 import urlparse |
| 17 |
| 18 from google.appengine.api import app_identity |
| 19 |
| 20 from third_party import ezt |
| 21 |
| 22 import settings |
| 23 from framework import actionlimit |
| 24 from framework import framework_constants |
| 25 from framework import template_helpers |
| 26 from framework import timestr |
| 27 from framework import urls |
| 28 from services import client_config_svc |
| 29 |
| 30 |
| 31 # For random key generation |
| 32 RANDOM_KEY_LENGTH = 128 |
| 33 RANDOM_KEY_CHARACTERS = string.ascii_letters + string.digits |
| 34 |
| 35 # params recognized by FormatURL, in the order they will appear in the url |
| 36 RECOGNIZED_PARAMS = ['can', 'start', 'num', 'q', 'colspec', 'groupby', 'sort', |
| 37 'show', 'format', 'me', 'table_title', 'projects'] |
| 38 |
| 39 |
| 40 def retry(tries, delay=1, backoff=2): |
| 41 """A retry decorator with exponential backoff. |
| 42 |
| 43 Functions are retried when Exceptions occur. |
| 44 |
| 45 Args: |
| 46 tries: int Number of times to retry, set to 0 to disable retry. |
| 47 delay: float Initial sleep time in seconds. |
| 48 backoff: float Must be greater than 1, further failures would sleep |
| 49 delay*=backoff seconds. |
| 50 """ |
| 51 if backoff <= 1: |
| 52 raise ValueError("backoff must be greater than 1") |
| 53 if tries < 0: |
| 54 raise ValueError("tries must be 0 or greater") |
| 55 if delay <= 0: |
| 56 raise ValueError("delay must be greater than 0") |
| 57 |
| 58 def decorator(func): |
| 59 def wrapper(*args, **kwargs): |
| 60 _tries, _delay = tries, delay |
| 61 _tries += 1 # Ensure we call func at least once. |
| 62 while _tries > 0: |
| 63 try: |
| 64 ret = func(*args, **kwargs) |
| 65 return ret |
| 66 except Exception: |
| 67 _tries -= 1 |
| 68 if _tries == 0: |
| 69 logging.error('Exceeded maximum number of retries for %s.', |
| 70 func.__name__) |
| 71 raise |
| 72 trace_str = traceback.format_exc() |
| 73 logging.warning('Retrying %s due to Exception: %s', |
| 74 func.__name__, trace_str) |
| 75 time.sleep(_delay) |
| 76 _delay *= backoff # Wait longer the next time we fail. |
| 77 return wrapper |
| 78 return decorator |
| 79 |
| 80 |
| 81 class PromiseCallback(object): |
| 82 """Executes the work of a Promise and then dereferences everything.""" |
| 83 |
| 84 def __init__(self, promise, callback, *args, **kwargs): |
| 85 self.promise = promise |
| 86 self.callback = callback |
| 87 self.args = args |
| 88 self.kwargs = kwargs |
| 89 |
| 90 def __call__(self): |
| 91 try: |
| 92 self.promise._WorkOnPromise(self.callback, *self.args, **self.kwargs) |
| 93 finally: |
| 94 # Make sure we no longer hold onto references to anything. |
| 95 self.promise = self.callback = self.args = self.kwargs = None |
| 96 |
| 97 |
| 98 class Promise(object): |
| 99 """Class for promises to deliver a value in the future. |
| 100 |
| 101 A thread is started to run callback(args), that thread |
| 102 should return the value that it generates, or raise an expception. |
| 103 p.WaitAndGetValue() will block until a value is available. |
| 104 If an exception was raised, p.WaitAndGetValue() will re-raise the |
| 105 same exception. |
| 106 """ |
| 107 |
| 108 def __init__(self, callback, *args, **kwargs): |
| 109 """Initialize the promise and immediately call the supplied function. |
| 110 |
| 111 Args: |
| 112 callback: Function that takes the args and returns the promise value. |
| 113 *args: Any arguments to the target function. |
| 114 **kwargs: Any keyword args for the target function. |
| 115 """ |
| 116 |
| 117 self.has_value = False |
| 118 self.value = None |
| 119 self.event = threading.Event() |
| 120 self.exception = None |
| 121 |
| 122 promise_callback = PromiseCallback(self, callback, *args, **kwargs) |
| 123 |
| 124 # Execute the callback in another thread. |
| 125 promise_thread = threading.Thread(target=promise_callback) |
| 126 promise_thread.start() |
| 127 |
| 128 def _WorkOnPromise(self, callback, *args, **kwargs): |
| 129 """Run callback to compute the promised value. Save any exceptions.""" |
| 130 try: |
| 131 self.value = callback(*args, **kwargs) |
| 132 except Exception as e: |
| 133 trace_str = traceback.format_exc() |
| 134 logging.info('Exception while working on promise: %s\n', trace_str) |
| 135 # Add the stack trace at this point to the exception. That way, in the |
| 136 # logs, we can see what happened further up in the call stack |
| 137 # than WaitAndGetValue(), which re-raises exceptions. |
| 138 e.pre_promise_trace = trace_str |
| 139 self.exception = e |
| 140 finally: |
| 141 self.has_value = True |
| 142 self.event.set() |
| 143 |
| 144 def WaitAndGetValue(self): |
| 145 """Block until my value is available, then return it or raise exception.""" |
| 146 self.event.wait() |
| 147 if self.exception: |
| 148 raise self.exception # pylint: disable=raising-bad-type |
| 149 return self.value |
| 150 |
| 151 |
| 152 def FormatAbsoluteURLForDomain( |
| 153 host, project_name, servlet_name, scheme='https', **kwargs): |
| 154 """A variant of FormatAbsoluteURL for when request objects are not available. |
| 155 |
| 156 Args: |
| 157 host: string with hostname and optional port, e.g. 'localhost:8080'. |
| 158 project_name: the destination project name, if any. |
| 159 servlet_name: site or project-local url fragement of dest page. |
| 160 scheme: url scheme, e.g., 'http' or 'https'. |
| 161 **kwargs: additional query string parameters may be specified as named |
| 162 arguments to this function. |
| 163 |
| 164 Returns: |
| 165 A full url beginning with 'http[s]://'. |
| 166 """ |
| 167 path_and_args = FormatURL(None, servlet_name, **kwargs) |
| 168 |
| 169 if host: |
| 170 domain_port = host.split(':') |
| 171 domain_port[0] = GetPreferredDomain(domain_port[0]) |
| 172 host = ':'.join(domain_port) |
| 173 |
| 174 absolute_domain_url = '%s://%s' % (scheme, host) |
| 175 if project_name: |
| 176 return '%s/p/%s%s' % (absolute_domain_url, project_name, path_and_args) |
| 177 return absolute_domain_url + path_and_args |
| 178 |
| 179 |
| 180 def FormatAbsoluteURL( |
| 181 mr, servlet_name, include_project=True, project_name=None, |
| 182 scheme=None, copy_params=True, **kwargs): |
| 183 """Return an absolute URL to a servlet with old and new params. |
| 184 |
| 185 Args: |
| 186 mr: info parsed from the current request. |
| 187 servlet_name: site or project-local url fragement of dest page. |
| 188 include_project: if True, include the project home url as part of the |
| 189 destination URL (as long as it is specified either in mr |
| 190 or as the project_name param.) |
| 191 project_name: the destination project name, to override |
| 192 mr.project_name if include_project is True. |
| 193 scheme: either 'http' or 'https', to override mr.request.scheme. |
| 194 copy_params: if True, copy well-known parameters from the existing request. |
| 195 **kwargs: additional query string parameters may be specified as named |
| 196 arguments to this function. |
| 197 |
| 198 Returns: |
| 199 A full url beginning with 'http[s]://'. The destination URL will be in |
| 200 the same domain as the current request. |
| 201 """ |
| 202 path_and_args = FormatURL( |
| 203 mr if copy_params else None, servlet_name, **kwargs) |
| 204 scheme = scheme or mr.request.scheme |
| 205 |
| 206 project_base = '' |
| 207 if include_project: |
| 208 project_base = '/p/%s' % (project_name or mr.project_name) |
| 209 |
| 210 return '%s://%s%s%s' % (scheme, mr.request.host, project_base, path_and_args) |
| 211 |
| 212 |
| 213 def FormatMovedProjectURL(mr, moved_to): |
| 214 """Return a transformation of the given url into the given project. |
| 215 |
| 216 Args: |
| 217 mr: common information parsed from the HTTP request. |
| 218 moved_to: A string from a project's moved_to field that matches |
| 219 framework_bizobj.RE_PROJECT_NAME. |
| 220 |
| 221 Returns: |
| 222 The url transposed into the given destination project. |
| 223 """ |
| 224 project_name = moved_to |
| 225 _, _, path, parameters, query, fragment_identifier = urlparse.urlparse( |
| 226 mr.current_page_url) |
| 227 # Strip off leading "/p/<moved from project>" |
| 228 path = '/' + path.split('/', 3)[3] |
| 229 rest_of_url = urlparse.urlunparse( |
| 230 ('', '', path, parameters, query, fragment_identifier)) |
| 231 return '/p/%s%s' % (project_name, rest_of_url) |
| 232 |
| 233 |
| 234 def FormatURL(mr, servlet_path, **kwargs): |
| 235 """Return a project relative URL to a servlet with old and new params.""" |
| 236 # Standard params not overridden in **kwargs come first, followed by kwargs. |
| 237 # The exception is the 'id' param. If present then the 'id' param always comes |
| 238 # first. See bugs.chromium.org/p/monorail/issues/detail?id=374 |
| 239 all_params = [] |
| 240 if kwargs.get('id'): |
| 241 all_params.append(('id', kwargs['id'])) |
| 242 if mr: |
| 243 all_params.extend( |
| 244 (name, mr.GetParam(name)) for name in RECOGNIZED_PARAMS |
| 245 if name not in kwargs) |
| 246 |
| 247 all_params.extend( |
| 248 # Ignore the 'id' param since we already added it above. |
| 249 sorted([kwarg for kwarg in kwargs.items() if kwarg[0] != 'id'])) |
| 250 return _FormatQueryString(servlet_path, all_params) |
| 251 |
| 252 |
| 253 def _FormatQueryString(url, params): |
| 254 """URLencode a list of parameters and attach them to the end of a URL.""" |
| 255 param_string = '&'.join( |
| 256 '%s=%s' % (name, urllib.quote(unicode(value).encode('utf-8'))) |
| 257 for name, value in params if value is not None) |
| 258 if not param_string: |
| 259 qs_start_char = '' |
| 260 elif '?' in url: |
| 261 qs_start_char = '&' |
| 262 else: |
| 263 qs_start_char = '?' |
| 264 return '%s%s%s' % (url, qs_start_char, param_string) |
| 265 |
| 266 |
| 267 def WordWrapSuperLongLines(s, max_cols=100): |
| 268 """Reformat input that was not word-wrapped by the browser. |
| 269 |
| 270 Args: |
| 271 s: the string to be word-wrapped, it may have embedded newlines. |
| 272 max_cols: int maximum line length. |
| 273 |
| 274 Returns: |
| 275 Wrapped text string. |
| 276 |
| 277 Rather than wrap the whole thing, we only wrap super-long lines and keep |
| 278 all the reasonable lines formated as-is. |
| 279 """ |
| 280 lines = [textwrap.fill(line, max_cols) for line in s.splitlines()] |
| 281 wrapped_text = '\n'.join(lines) |
| 282 |
| 283 # The split/join logic above can lose one final blank line. |
| 284 if s.endswith('\n') or s.endswith('\r'): |
| 285 wrapped_text += '\n' |
| 286 |
| 287 return wrapped_text |
| 288 |
| 289 |
| 290 def StaticCacheHeaders(): |
| 291 """Returns HTTP headers for static content, based on the current time.""" |
| 292 year_from_now = int(time.time()) + framework_constants.SECS_PER_YEAR |
| 293 headers = [ |
| 294 ('Cache-Control', |
| 295 'max-age=%d, private' % framework_constants.SECS_PER_YEAR), |
| 296 ('Last-Modified', timestr.TimeForHTMLHeader()), |
| 297 ('Expires', timestr.TimeForHTMLHeader(when=year_from_now)), |
| 298 ] |
| 299 logging.info('static headers are %r', headers) |
| 300 return headers |
| 301 |
| 302 |
| 303 def ComputeListDeltas(old_list, new_list): |
| 304 """Given an old and new list, return the items added and removed. |
| 305 |
| 306 Args: |
| 307 old_list: old list of values for comparison. |
| 308 new_list: new list of values for comparison. |
| 309 |
| 310 Returns: |
| 311 Two lists: one with all the values added (in new_list but was not |
| 312 in old_list), and one with all the values removed (not in new_list |
| 313 but was in old_lit). |
| 314 """ |
| 315 if old_list == new_list: |
| 316 return [], [] # A common case: nothing was added or removed. |
| 317 |
| 318 added = set(new_list) |
| 319 added.difference_update(old_list) |
| 320 removed = set(old_list) |
| 321 removed.difference_update(new_list) |
| 322 return list(added), list(removed) |
| 323 |
| 324 |
| 325 def GetRoleName(effective_ids, project): |
| 326 """Determines the name of the role a member has for a given project. |
| 327 |
| 328 Args: |
| 329 effective_ids: set of user IDs to get the role name for. |
| 330 project: Project PB containing the different the different member lists. |
| 331 |
| 332 Returns: |
| 333 The name of the role. |
| 334 """ |
| 335 if not effective_ids.isdisjoint(project.owner_ids): |
| 336 return 'Owner' |
| 337 if not effective_ids.isdisjoint(project.committer_ids): |
| 338 return 'Committer' |
| 339 if not effective_ids.isdisjoint(project.contributor_ids): |
| 340 return 'Contributor' |
| 341 return None |
| 342 |
| 343 |
| 344 class UserSettings(object): |
| 345 """Abstract class providing static methods for user settings forms.""" |
| 346 |
| 347 @classmethod |
| 348 def GatherUnifiedSettingsPageData( |
| 349 cls, logged_in_user_id, settings_user_view, settings_user): |
| 350 """Gather EZT variables needed for the unified user settings form. |
| 351 |
| 352 Args: |
| 353 logged_in_user_id: The user ID of the acting user. |
| 354 settings_user_view: The UserView of the target user. |
| 355 settings_user: The User PB of the target user. |
| 356 |
| 357 Returns: |
| 358 A dictionary giving the names and values of all the variables to |
| 359 be exported to EZT to support the unified user settings form template. |
| 360 """ |
| 361 |
| 362 def ActionLastReset(action_limit): |
| 363 """Return a formatted time string for the last action limit reset.""" |
| 364 if action_limit: |
| 365 return time.asctime(time.localtime(action_limit.reset_timestamp)) |
| 366 return 'Never' |
| 367 |
| 368 def DefaultLifetimeLimit(action_type): |
| 369 """Return the deault lifetime limit for the give type of action.""" |
| 370 return actionlimit.ACTION_LIMITS[action_type][3] |
| 371 |
| 372 def DefaultPeriodSoftLimit(action_type): |
| 373 """Return the deault period soft limit for the give type of action.""" |
| 374 return actionlimit.ACTION_LIMITS[action_type][1] |
| 375 |
| 376 def DefaultPeriodHardLimit(action_type): |
| 377 """Return the deault period jard limit for the give type of action.""" |
| 378 return actionlimit.ACTION_LIMITS[action_type][2] |
| 379 |
| 380 project_creation_lifetime_limit = ( |
| 381 (settings_user.project_creation_limit and |
| 382 settings_user.project_creation_limit.lifetime_limit) or |
| 383 DefaultLifetimeLimit(actionlimit.PROJECT_CREATION)) |
| 384 project_creation_soft_limit = ( |
| 385 (settings_user.project_creation_limit and |
| 386 settings_user.project_creation_limit.period_soft_limit) or |
| 387 DefaultPeriodSoftLimit(actionlimit.PROJECT_CREATION)) |
| 388 project_creation_hard_limit = ( |
| 389 (settings_user.project_creation_limit and |
| 390 settings_user.project_creation_limit.period_hard_limit) or |
| 391 DefaultPeriodHardLimit(actionlimit.PROJECT_CREATION)) |
| 392 issue_comment_lifetime_limit = ( |
| 393 (settings_user.issue_comment_limit and |
| 394 settings_user.issue_comment_limit.lifetime_limit) or |
| 395 DefaultLifetimeLimit(actionlimit.ISSUE_COMMENT)) |
| 396 issue_comment_soft_limit = ( |
| 397 (settings_user.issue_comment_limit and |
| 398 settings_user.issue_comment_limit.period_soft_limit) or |
| 399 DefaultPeriodSoftLimit(actionlimit.ISSUE_COMMENT)) |
| 400 issue_comment_hard_limit = ( |
| 401 (settings_user.issue_comment_limit and |
| 402 settings_user.issue_comment_limit.period_hard_limit) or |
| 403 DefaultPeriodHardLimit(actionlimit.ISSUE_COMMENT )) |
| 404 issue_attachment_lifetime_limit = ( |
| 405 (settings_user.issue_attachment_limit and |
| 406 settings_user.issue_attachment_limit.lifetime_limit) or |
| 407 DefaultLifetimeLimit(actionlimit.ISSUE_ATTACHMENT)) |
| 408 issue_attachment_soft_limit = ( |
| 409 (settings_user.issue_attachment_limit and |
| 410 settings_user.issue_attachment_limit.period_soft_limit) or |
| 411 DefaultPeriodSoftLimit(actionlimit.ISSUE_ATTACHMENT)) |
| 412 issue_attachment_hard_limit = ( |
| 413 (settings_user.issue_attachment_limit and |
| 414 settings_user.issue_attachment_limit.period_hard_limit) or |
| 415 DefaultPeriodHardLimit(actionlimit.ISSUE_ATTACHMENT)) |
| 416 issue_bulk_edit_lifetime_limit = ( |
| 417 (settings_user.issue_bulk_edit_limit and |
| 418 settings_user.issue_bulk_edit_limit.lifetime_limit) or |
| 419 DefaultLifetimeLimit(actionlimit.ISSUE_BULK_EDIT)) |
| 420 issue_bulk_edit_soft_limit = ( |
| 421 (settings_user.issue_bulk_edit_limit and |
| 422 settings_user.issue_bulk_edit_limit.period_soft_limit) or |
| 423 DefaultPeriodSoftLimit(actionlimit.ISSUE_BULK_EDIT)) |
| 424 issue_bulk_edit_hard_limit = ( |
| 425 (settings_user.issue_bulk_edit_limit and |
| 426 settings_user.issue_bulk_edit_limit.period_hard_limit) or |
| 427 DefaultPeriodHardLimit(actionlimit.ISSUE_BULK_EDIT)) |
| 428 api_request_lifetime_limit = ( |
| 429 (settings_user.api_request_limit and |
| 430 settings_user.api_request_limit.lifetime_limit) or |
| 431 DefaultLifetimeLimit(actionlimit.API_REQUEST)) |
| 432 api_request_soft_limit = ( |
| 433 (settings_user.api_request_limit and |
| 434 settings_user.api_request_limit.period_soft_limit) or |
| 435 DefaultPeriodSoftLimit(actionlimit.API_REQUEST)) |
| 436 api_request_hard_limit = ( |
| 437 (settings_user.api_request_limit and |
| 438 settings_user.api_request_limit.period_hard_limit) or |
| 439 DefaultPeriodHardLimit(actionlimit.API_REQUEST)) |
| 440 |
| 441 return { |
| 442 'settings_user': settings_user_view, |
| 443 'settings_user_pb': template_helpers.PBProxy(settings_user), |
| 444 'settings_user_is_banned': ezt.boolean(settings_user.banned), |
| 445 'settings_user_ignore_action_limits': ( |
| 446 ezt.boolean(settings_user.ignore_action_limits)), |
| 447 'self': ezt.boolean(logged_in_user_id == settings_user_view.user_id), |
| 448 'project_creation_reset': ( |
| 449 ActionLastReset(settings_user.project_creation_limit)), |
| 450 'issue_comment_reset': ( |
| 451 ActionLastReset(settings_user.issue_comment_limit)), |
| 452 'issue_attachment_reset': ( |
| 453 ActionLastReset(settings_user.issue_attachment_limit)), |
| 454 'issue_bulk_edit_reset': ( |
| 455 ActionLastReset(settings_user.issue_bulk_edit_limit)), |
| 456 'api_request_reset': ( |
| 457 ActionLastReset(settings_user.api_request_limit)), |
| 458 'project_creation_lifetime_limit': project_creation_lifetime_limit, |
| 459 'project_creation_soft_limit': project_creation_soft_limit, |
| 460 'project_creation_hard_limit': project_creation_hard_limit, |
| 461 'issue_comment_lifetime_limit': issue_comment_lifetime_limit, |
| 462 'issue_comment_soft_limit': issue_comment_soft_limit, |
| 463 'issue_comment_hard_limit': issue_comment_hard_limit, |
| 464 'issue_attachment_lifetime_limit': issue_attachment_lifetime_limit, |
| 465 'issue_attachment_soft_limit': issue_attachment_soft_limit, |
| 466 'issue_attachment_hard_limit': issue_attachment_hard_limit, |
| 467 'issue_bulk_edit_lifetime_limit': issue_bulk_edit_lifetime_limit, |
| 468 'issue_bulk_edit_soft_limit': issue_bulk_edit_soft_limit, |
| 469 'issue_bulk_edit_hard_limit': issue_bulk_edit_hard_limit, |
| 470 'api_request_lifetime_limit': api_request_lifetime_limit, |
| 471 'api_request_soft_limit': api_request_soft_limit, |
| 472 'api_request_hard_limit': api_request_hard_limit, |
| 473 'profile_url_fragment': ( |
| 474 settings_user_view.profile_url[len('/u/'):]), |
| 475 'preview_on_hover': ezt.boolean(settings_user.preview_on_hover), |
| 476 } |
| 477 |
| 478 @classmethod |
| 479 def ProcessSettingsForm( |
| 480 cls, cnxn, user_service, post_data, user_id, user, admin=False): |
| 481 """Process the posted form data from the unified user settings form. |
| 482 |
| 483 Args: |
| 484 cnxn: connection to the SQL database. |
| 485 user_service: An instance of UserService for saving changes. |
| 486 post_data: The parsed post data from the form submission request. |
| 487 user_id: The user id of the target user. |
| 488 user: The user PB of the target user. |
| 489 admin: Whether settings reserved for admins are supported. |
| 490 """ |
| 491 obscure_email = 'obscure_email' in post_data |
| 492 |
| 493 kwargs = {} |
| 494 if admin: |
| 495 kwargs.update(is_site_admin='site_admin' in post_data, |
| 496 ignore_action_limits='ignore_action_limits' in post_data) |
| 497 kwargs.update(is_banned='banned' in post_data, |
| 498 banned_reason=post_data.get('banned_reason', '')) |
| 499 |
| 500 # action limits |
| 501 action_limit_updates = {} |
| 502 for action_name in actionlimit.ACTION_TYPE_NAMES.iterkeys(): |
| 503 reset_input = 'reset_' + action_name |
| 504 lifetime_input = action_name + '_lifetime_limit' |
| 505 soft_input = action_name + '_soft_limit' |
| 506 hard_input = action_name + '_hard_limit' |
| 507 pb_getter = action_name + '_limit' |
| 508 old_lifetime_limit = getattr(user, pb_getter).lifetime_limit |
| 509 old_soft_limit = getattr(user, pb_getter).period_soft_limit |
| 510 old_hard_limit = getattr(user, pb_getter).period_hard_limit |
| 511 |
| 512 # Try and get the new limit from post data. |
| 513 # If the user doesn't use an integer, act as if no change requested. |
| 514 def _GetLimit(post_data, limit_input, old_limit): |
| 515 try: |
| 516 new_limit = int(post_data[limit_input]) |
| 517 except (KeyError, ValueError): |
| 518 new_limit = old_limit |
| 519 return new_limit |
| 520 |
| 521 new_lifetime_limit = _GetLimit(post_data, lifetime_input, |
| 522 old_lifetime_limit) |
| 523 new_soft_limit = _GetLimit(post_data, soft_input, |
| 524 old_soft_limit) |
| 525 new_hard_limit = _GetLimit(post_data, hard_input, |
| 526 old_hard_limit) |
| 527 |
| 528 if ((new_lifetime_limit >= 0 and |
| 529 new_lifetime_limit != old_lifetime_limit) or |
| 530 (new_soft_limit >= 0 and new_soft_limit != old_soft_limit) or |
| 531 (new_hard_limit >= 0 and new_hard_limit != old_hard_limit)): |
| 532 action_limit_updates[action_name] = ( |
| 533 new_soft_limit, new_hard_limit, new_lifetime_limit) |
| 534 elif reset_input in post_data: |
| 535 action_limit_updates[action_name] = None |
| 536 kwargs.update(action_limit_updates=action_limit_updates) |
| 537 |
| 538 user_service.UpdateUserSettings( |
| 539 cnxn, user_id, user, notify='notify' in post_data, |
| 540 notify_starred='notify_starred' in post_data, |
| 541 preview_on_hover='preview_on_hover' in post_data, |
| 542 obscure_email=obscure_email, **kwargs) |
| 543 |
| 544 |
| 545 def GetHostPort(): |
| 546 """Get string domain name and port number.""" |
| 547 |
| 548 app_id = app_identity.get_application_id() |
| 549 if ':' in app_id: |
| 550 domain, app_id = app_id.split(':') |
| 551 else: |
| 552 domain = '' |
| 553 |
| 554 if domain.startswith('google'): |
| 555 hostport = '%s.googleplex.com' % app_id |
| 556 else: |
| 557 hostport = '%s.appspot.com' % app_id |
| 558 |
| 559 return GetPreferredDomain(hostport) |
| 560 |
| 561 |
| 562 def IssueCommentURL(hostport, project, local_id, seq_num=None): |
| 563 """Return a URL pointing directly to the specified comment.""" |
| 564 detail_url = FormatAbsoluteURLForDomain( |
| 565 hostport, project.project_name, urls.ISSUE_DETAIL, id=local_id) |
| 566 if seq_num: |
| 567 detail_url += '#c%d' % seq_num |
| 568 |
| 569 return detail_url |
| 570 |
| 571 |
| 572 def MurmurHash3_x86_32(key, seed=0x0): |
| 573 """Implements the x86/32-bit version of Murmur Hash 3.0. |
| 574 |
| 575 MurmurHash3 is written by Austin Appleby, and is placed in the public |
| 576 domain. See https://code.google.com/p/smhasher/ for details. |
| 577 |
| 578 This pure python implementation of the x86/32 bit version of MurmurHash3 is |
| 579 written by Fredrik Kihlander and also placed in the public domain. |
| 580 See https://github.com/wc-duck/pymmh3 for details. |
| 581 |
| 582 The MurmurHash3 algorithm is chosen for these reasons: |
| 583 * It is fast, even when implemented in pure python. |
| 584 * It is remarkably well distributed, and unlikely to cause collisions. |
| 585 * It is stable and unchanging (any improvements will be in MurmurHash4). |
| 586 * It is well-tested, and easily usable in other contexts (such as bulk |
| 587 data imports). |
| 588 |
| 589 Args: |
| 590 key (string): the data that you want hashed |
| 591 seed (int): An offset, treated as essentially part of the key. |
| 592 |
| 593 Returns: |
| 594 A 32-bit integer (can be interpreted as either signed or unsigned). |
| 595 """ |
| 596 key = bytearray(key.encode('utf-8')) |
| 597 |
| 598 def fmix(h): |
| 599 h ^= h >> 16 |
| 600 h = (h * 0x85ebca6b) & 0xFFFFFFFF |
| 601 h ^= h >> 13 |
| 602 h = (h * 0xc2b2ae35) & 0xFFFFFFFF |
| 603 h ^= h >> 16 |
| 604 return h; |
| 605 |
| 606 length = len(key) |
| 607 nblocks = int(length / 4) |
| 608 |
| 609 h1 = seed; |
| 610 |
| 611 c1 = 0xcc9e2d51 |
| 612 c2 = 0x1b873593 |
| 613 |
| 614 # body |
| 615 for block_start in xrange(0, nblocks * 4, 4): |
| 616 k1 = key[ block_start + 3 ] << 24 | \ |
| 617 key[ block_start + 2 ] << 16 | \ |
| 618 key[ block_start + 1 ] << 8 | \ |
| 619 key[ block_start + 0 ] |
| 620 |
| 621 k1 = c1 * k1 & 0xFFFFFFFF |
| 622 k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF |
| 623 k1 = (c2 * k1) & 0xFFFFFFFF; |
| 624 |
| 625 h1 ^= k1 |
| 626 h1 = ( h1 << 13 | h1 >> 19 ) & 0xFFFFFFFF |
| 627 h1 = ( h1 * 5 + 0xe6546b64 ) & 0xFFFFFFFF |
| 628 |
| 629 # tail |
| 630 tail_index = nblocks * 4 |
| 631 k1 = 0 |
| 632 tail_size = length & 3 |
| 633 |
| 634 if tail_size >= 3: |
| 635 k1 ^= key[ tail_index + 2 ] << 16 |
| 636 if tail_size >= 2: |
| 637 k1 ^= key[ tail_index + 1 ] << 8 |
| 638 if tail_size >= 1: |
| 639 k1 ^= key[ tail_index + 0 ] |
| 640 |
| 641 if tail_size != 0: |
| 642 k1 = ( k1 * c1 ) & 0xFFFFFFFF |
| 643 k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF |
| 644 k1 = ( k1 * c2 ) & 0xFFFFFFFF |
| 645 h1 ^= k1 |
| 646 |
| 647 return fmix( h1 ^ length ) |
| 648 |
| 649 |
| 650 def MakeRandomKey(length=RANDOM_KEY_LENGTH, chars=RANDOM_KEY_CHARACTERS): |
| 651 """Return a string with lots of random characters.""" |
| 652 chars = [random.choice(chars) for _ in range(length)] |
| 653 return ''.join(chars) |
| 654 |
| 655 |
| 656 def IsServiceAccount(email): |
| 657 """Return a boolean value whether this email is a service account.""" |
| 658 if email.endswith('gserviceaccount.com'): |
| 659 return True |
| 660 _, client_emails = ( |
| 661 client_config_svc.GetClientConfigSvc().GetClientIDEmails()) |
| 662 return email in client_emails |
| 663 |
| 664 |
| 665 def GetPreferredDomain(domain): |
| 666 """Get preferred domain to display. |
| 667 |
| 668 The preferred domain replaces app_id for default version of monorail-prod |
| 669 and monorail-staging. |
| 670 """ |
| 671 return settings.preferred_domains.get(domain, domain) |
OLD | NEW |