class BaseHandler(webapp.RequestHandler): # Handlers that don't need a repository name can set this to False. repo_required = True # Handlers that require HTTPS can set this to True. https_required = False # Set this to True to enable a handler even for deactivated repositories. ignore_deactivation = False # Handlers that require an admin permission must set this to True. admin_required = False # List all accepted query parameters here with their associated validators. auto_params = { 'add_note': validate_yes, 'age': validate_age, 'alternate_family_names': strip, 'alternate_given_names': strip, 'author_email': strip, 'author_made_contact': validate_yes, 'author_name': strip, 'author_phone': strip, 'believed_dead_permission': validate_checkbox_as_bool, 'cache_seconds': validate_cache_seconds, 'clone': validate_yes, 'confirm': validate_yes, 'contact_email': strip, 'contact_name': strip, 'content_id': strip, 'context': strip, 'cursor': strip, 'date_of_birth': validate_approximate_date, 'description': strip, 'domain_write_permission': strip, 'dupe_notes': validate_yes, 'email_of_found_person': strip, 'error': strip, 'expiry_option': validate_expiry, 'family_name': strip, 'full_read_permission': validate_checkbox_as_bool, 'given_name': strip, 'home_city': strip, 'home_country': strip, 'home_neighborhood': strip, 'home_postal_code': strip, 'home_state': strip, 'home_street': strip, 'id': strip, 'id1': strip, 'id2': strip, 'id3': strip, 'is_valid': validate_checkbox_as_bool, 'key': strip, 'lang': validate_lang, 'last_known_location': strip, 'mark_notes_reviewed': validate_checkbox_as_bool, 'max_results': validate_int, 'min_entry_date': validate_datetime, 'new_repo': validate_repo, 'note_photo': validate_image, 'note_photo_url': strip, 'omit_notes': validate_yes, 'operation': strip, 'organization_name': strip, 'person_record_id': strip, 'phone_of_found_person': strip, 'photo': validate_image, 'photo_url': strip, 'profile_url1': strip, 'profile_url2': strip, 'profile_url3': strip, 'query': strip, 'query_type': strip, 'read_permission': validate_checkbox_as_bool, 'referrer': strip, 'resource_bundle': validate_resource_name, 'resource_bundle_default': validate_resource_name, 'resource_bundle_original': validate_resource_name, 'resource_lang': validate_lang, 'resource_name': validate_resource_name, 'role': validate_role, 'search_engine_id': validate_int, 'search_permission': validate_checkbox_as_bool, 'sex': validate_sex, 'signature': strip, 'skip': validate_int, 'small': validate_yes, 'source': strip, 'source_date': strip, 'source_name': strip, 'source_url': strip, 'stats_permission': validate_checkbox_as_bool, 'status': validate_status, 'style': strip, 'subscribe': validate_checkbox, 'subscribe_email': strip, 'subscribe_permission': validate_checkbox_as_bool, 'suppress_redirect': validate_yes, 'target': strip, 'text': strip, 'ui': strip_and_lower, 'utcnow': validate_timestamp, 'version': validate_version, } def redirect(self, path, repo=None, permanent=False, **params): # This will prepend the repo to the path to create a working URL, # unless the path has a global prefix or is an absolute URL. if re.match('^[a-z]+:', path) or GLOBAL_PATH_RE.match(path): if params: path += '?' + urlencode(params, self.charset) else: path = self.get_url(path, repo, **params) return webapp.RequestHandler.redirect(self, path, permanent=permanent) def render(self, name, language_override=None, cache_seconds=0, get_vars=lambda: {}, **vars): """Renders a template to the output stream, passing in the variables specified in **vars as well as any additional variables returned by get_vars(). Since this is intended for use by a dynamic page handler, caching is off by default; if cache_seconds is positive, then get_vars() will be called only when cached content is unavailable.""" self.write( self.render_to_string(name, language_override, cache_seconds, get_vars, **vars)) def render_to_string(self, name, language_override=None, cache_seconds=0, get_vars=lambda: {}, **vars): """Renders a template to a string, passing in the variables specified in **vars as well as any additional variables returned by get_vars(). Since this is intended for use by a dynamic page handler, caching is off by default; if cache_seconds is positive, then get_vars() will be called only when cached content is unavailable.""" # TODO(kpy): Make the contents of extra_key overridable by callers? lang = language_override or self.env.lang extra_key = (self.env.repo, self.env.charset, self.request.query_string) def get_all_vars(): vars.update(get_vars()) for key in ('env', 'config', 'params'): if key in vars: raise Exception( 'Cannot use "%s" as a key in vars. It is reserved.' % key) vars['env'] = self.env # pass along application-wide context vars['config'] = self.config # pass along the configuration vars['params'] = self.params # pass along the query parameters return vars return resources.get_rendered(name, lang, extra_key, get_all_vars, cache_seconds) def error(self, code, message='', message_html=''): self.info(code, message, message_html, style='error') def info(self, code, message='', message_html='', style='info'): """Renders a simple page with a message. Args: code: HTTP status code. message: A message in plain text. message_html: A message in HTML. style: 'info', 'error' or 'plain'. 'info' and 'error' differs in appearance. 'plain' just renders the message without extra HTML tags. Good for API response. """ is_error = 400 <= code < 600 if is_error: webapp.RequestHandler.error(self, code) else: self.response.set_status(code) if not message and not message_html: message = '%d: %s' % (code, httplib.responses.get(code)) if style == 'plain': self.__render_plain_message(message, message_html) else: try: self.render('message.html', cls=style, message=message, message_html=message_html) except: self.__render_plain_message(message, message_html) self.terminate_response() def __render_plain_message(self, message, message_html): self.response.out.write( django.utils.html.escape(message) + ('<p>' if message and message_html else '') + message_html) def terminate_response(self): """Prevents any further output from being written.""" self.response.out.write = lambda *args: None self.get = lambda *args: None self.post = lambda *args: None def write(self, text): """Sends text to the client using the charset from select_charset().""" self.response.out.write(text.encode(self.env.charset, 'replace')) def get_url(self, action, repo=None, scheme=None, **params): """Constructs the absolute URL for a given action and query parameters, preserving the current repo and the parameters listed in PRESERVED_QUERY_PARAM_NAMES.""" return get_url(self.request, repo or self.env.repo, action, charset=self.env.charset, scheme=scheme, **params) @staticmethod def add_task_for_repo(repo, name, action, **kwargs): """Queues up a task for an individual repository.""" task_name = '%s-%s-%s' % (repo, name, int(time.time() * 1000)) path = '/%s/%s' % (repo, action) taskqueue.add(name=task_name, method='GET', url=path, params=kwargs) def send_mail(self, to, subject, body): """Sends e-mail using a sender address that's allowed for this app.""" app_id = get_app_name() sender = 'Do not reply <do-not-reply@%s.%s>' % (app_id, EMAIL_DOMAIN) logging.info('Add mail task: recipient %r, subject %r' % (to, subject)) taskqueue.add(queue_name='send-mail', url='/global/admin/send_mail', params={ 'sender': sender, 'to': to, 'subject': subject, 'body': body }) def get_captcha_html(self, error_code=None, use_ssl=False): """Generates the necessary HTML to display a CAPTCHA validation box.""" # We use the 'custom_translations' parameter for UI messages, whereas # the 'lang' parameter controls the language of the challenge itself. # reCAPTCHA falls back to 'en' if this parameter isn't recognized. lang = self.env.lang.split('-')[0] return captcha.get_display_html( site_key=config.get('captcha_site_key'), use_ssl=use_ssl, error=error_code, lang=lang) def get_captcha_response(self): """Returns an object containing the CAPTCHA response information for the given request's CAPTCHA field information.""" captcha_response = self.request.get('g-recaptcha-response') return captcha.submit(captcha_response) def handle_exception(self, exception, debug_mode): logging.error(traceback.format_exc()) self.error( 500, _('There was an error processing your request. Sorry for the ' 'inconvenience. Our administrators will investigate the source ' 'of the problem, but please check that the format of your ' 'request is correct.')) def __get_env_language_for_babel(self): language_code = self.env.lang # A hack to avoid rejecting zh-hk locale. # This corresponds to the hack with LANGUAGE_SYNONYMS in const.py. # TODO: remove these 2 lines when a original code can be passed to here. if language_code == 'zhhk': language_code = 'zh-hk' try: return babel.Locale.parse(language_code, sep='-') except babel.UnknownLocaleError as e: # fallback language return babel.Locale('en') def to_local_time(self, date): """Converts a datetime object to the local time configured for the current repository. For convenience, returns None if date is None.""" # TODO(kpy): This only works for repositories that have a single fixed # time zone offset and never use Daylight Saving Time. if date: if self.config.time_zone_offset: return date + timedelta(0, 3600 * self.config.time_zone_offset) return date def format_datetime_localized(self, dt): """Formats a datetime object to a localized human-readable string based on the current locale.""" return format_datetime(dt, locale=self.__get_env_language_for_babel()) def to_formatted_local_time(self, dt): """Converts a datetime object to the local time configured for the current repository and formats to a localized human-readable string based on the current locale.""" dt = self.to_local_time(dt) return self.format_datetime_localized(dt) def maybe_redirect_for_repo_alias(self, request): """If the specified repository name is an alias, redirects to the URL with the canonical repository name and returns True. Otherwise returns False. """ # Config repo_alias is a dictionary from a repository name alias to # its canonical. # e.g., {'yol': '2013-yolanda', 'jam': '2014-jammu-kashmir-floods'} # # A repository name alias can be used instead of the canonical # repository name in URLs. This is especially useful combined with # the short URL. e.g., You can access # https://www.google.org/personfinder/2014-jammu-kashmir-floods # by http://g.co/pf/jam . if not self.repo: return False repo_aliases = config.get('repo_aliases', default={}) if self.repo in repo_aliases: canonical_repo = repo_aliases[self.repo] params = {} for name in request.arguments(): params[name] = request.get(name) # Redirects to the same URL including the query parameters, except # for the repository name. self.redirect('/' + self.env.action, repo=canonical_repo, **params) self.terminate_response() return True else: return False def __init__(self, request, response, env): webapp.RequestHandler.__init__(self, request, response) self.params = Struct() self.env = env self.repo = env.repo self.config = env.config self.charset = env.charset # Set default Content-Type header. self.response.headers['Content-Type'] = ('text/html; charset=%s' % self.charset) # Validate query parameters. for name, validator in self.auto_params.items(): try: value = self.request.get(name, '') setattr(self.params, name, validator(value)) except Exception, e: setattr(self.params, name, validator(None)) return self.error(400, 'Invalid parameter %s: %s' % (name, e)) # Ensure referrer is in whitelist, if it exists if self.params.referrer and (not self.params.referrer in self.config.referrer_whitelist): setattr(self.params, 'referrer', '') # Log the User-Agent header. sample_rate = float(self.config and self.config.user_agent_sample_rate or 0) if random.random() < sample_rate: model.UserAgentLog( repo=self.repo, sample_rate=sample_rate, user_agent=self.request.headers.get('User-Agent'), lang=lang, accept_charset=self.request.headers.get('Accept-Charset', ''), ip_address=self.request.remote_addr).put() # Check for SSL (unless running on localhost for development). if self.https_required and self.env.domain != 'localhost': if self.env.scheme != 'https': return self.error(403, 'HTTPS is required.') # Handles repository alias. if self.maybe_redirect_for_repo_alias(request): return # Check for an authorization key. self.auth = None if self.params.key: if self.repo: # check for domain specific one. self.auth = model.Authorization.get(self.repo, self.params.key) if not self.auth: # perhaps this is a global key ('*' for consistency with config). self.auth = model.Authorization.get('*', self.params.key) if self.auth and not self.auth.is_valid: self.auth = None # Shows a custom error page here when the user is not an admin # instead of "login: admin" in app.yaml # If we use it, user can't sign out # because the error page of "login: admin" doesn't have sign-out link. if self.admin_required: user = users.get_current_user() if not user: login_url = users.create_login_url(self.request.url) webapp.RequestHandler.redirect(self, login_url) self.terminate_response() return if not users.is_current_user_admin(): logout_url = users.create_logout_url(self.request.url) self.render('not_admin_error.html', logout_url=logout_url, user=user) self.terminate_response() return # Handlers that don't need a repository configuration can skip it. if not self.repo: if self.repo_required: return self.error(400, 'No repository specified.') return # Everything after this requires a repo. # Reject requests for repositories that don't exist. if not model.Repo.get_by_key_name(self.repo): html = 'No such repository. ' if self.env.repo_options: html += 'Select:<p>' + self.render_to_string('repo-menu.html') return self.error(404, message_html=html) # If this repository has been deactivated, terminate with a message. # The ignore_deactivation flag is for admin pages that bypass this. if self.config.deactivated and not self.ignore_deactivation: self.env.language_menu = [] self.env.robots_ok = True self.render('message.html', cls='deactivation', message_html=self.config.deactivation_message_html) self.terminate_response()
class BaseHandler(webapp.RequestHandler): # Handlers that don't need a repository name can set this to False. repo_required = True # Handlers that require HTTPS can set this to True. https_required = False # Set this to True to enable a handler even for deactivated repositories. ignore_deactivation = False # List all accepted query parameters here with their associated validators. auto_params = { 'add_note': validate_yes, 'age': validate_age, 'alternate_family_names': strip, 'alternate_given_names': strip, 'author_email': strip, 'author_made_contact': validate_yes, 'author_name': strip, 'author_phone': strip, 'believed_dead_permission': validate_checkbox_as_bool, 'cache_seconds': validate_cache_seconds, 'clone': validate_yes, 'confirm': validate_yes, 'contact_email': strip, 'contact_name': strip, 'content_id': strip, 'cursor': strip, 'date_of_birth': validate_approximate_date, 'description': strip, 'domain_write_permission': strip, 'dupe_notes': validate_yes, 'email_of_found_person': strip, 'error': strip, 'expiry_option': validate_expiry, 'family_name': strip, 'full_read_permission': validate_checkbox_as_bool, 'given_name': strip, 'home_city': strip, 'home_country': strip, 'home_neighborhood': strip, 'home_postal_code': strip, 'home_state': strip, 'home_street': strip, 'id': strip, 'id1': strip, 'id2': strip, 'id3': strip, 'is_valid': validate_checkbox_as_bool, 'key': strip, 'lang': validate_lang, 'last_known_location': strip, 'mark_notes_reviewed': validate_checkbox_as_bool, 'max_results': validate_int, 'min_entry_date': validate_datetime, 'new_repo': validate_repo, 'note_photo': validate_image, 'note_photo_url': strip, 'omit_notes': validate_yes, 'operation': strip, 'organization_name': strip, 'person_record_id': strip, 'phone_of_found_person': strip, 'photo': validate_image, 'photo_url': strip, 'profile_url1': strip, 'profile_url2': strip, 'profile_url3': strip, 'query': strip, 'query_type': strip, 'read_permission': validate_checkbox_as_bool, 'referrer': strip, 'resource_bundle': validate_resource_name, 'resource_bundle_default': validate_resource_name, 'resource_bundle_original': validate_resource_name, 'resource_lang': validate_lang, 'resource_name': validate_resource_name, 'role': validate_role, 'search_engine_id': validate_int, 'search_permission': validate_checkbox_as_bool, 'sex': validate_sex, 'signature': strip, 'skip': validate_int, 'small': validate_yes, 'source': strip, 'source_date': strip, 'source_name': strip, 'source_url': strip, 'stats_permission': validate_checkbox_as_bool, 'status': validate_status, 'style': strip, 'subscribe': validate_checkbox, 'subscribe_email': strip, 'subscribe_permission': validate_checkbox_as_bool, 'suppress_redirect': validate_yes, 'target': strip, 'text': strip, 'ui': strip_and_lower, 'utcnow': validate_timestamp, 'version': validate_version, } def redirect(self, path, repo=None, permanent=False, **params): # This will prepend the repo to the path to create a working URL, # unless the path has a global prefix or is an absolute URL. if re.match('^[a-z]+:', path) or GLOBAL_PATH_RE.match(path): if params: path += '?' + urlencode(params, self.charset) else: path = self.get_url(path, repo, **params) return webapp.RequestHandler.redirect(self, path, permanent=permanent) def render(self, name, language_override=None, cache_seconds=0, get_vars=lambda: {}, **vars): """Renders a template to the output stream, passing in the variables specified in **vars as well as any additional variables returned by get_vars(). Since this is intended for use by a dynamic page handler, caching is off by default; if cache_seconds is positive, then get_vars() will be called only when cached content is unavailable.""" self.write( self.render_to_string(name, language_override, cache_seconds, get_vars, **vars)) def render_to_string(self, name, language_override=None, cache_seconds=0, get_vars=lambda: {}, **vars): """Renders a template to a string, passing in the variables specified in **vars as well as any additional variables returned by get_vars(). Since this is intended for use by a dynamic page handler, caching is off by default; if cache_seconds is positive, then get_vars() will be called only when cached content is unavailable.""" # TODO(kpy): Make the contents of extra_key overridable by callers? lang = language_override or self.env.lang extra_key = (self.env.repo, self.env.charset, self.request.query_string) def get_all_vars(): vars['env'] = self.env # pass along application-wide context vars['config'] = self.config # pass along the configuration vars['params'] = self.params # pass along the query parameters vars.update(get_vars()) return vars return resources.get_rendered(name, lang, extra_key, get_all_vars, cache_seconds) def error(self, code, message='', message_html=''): self.info(code, message, message_html, style='error') def info(self, code, message='', message_html='', style='info'): """Renders a simple page with a message. Args: code: HTTP status code. message: A message in plain text. message_html: A message in HTML. style: 'info', 'error' or 'plain'. 'info' and 'error' differs in appearance. 'plain' just renders the message without extra HTML tags. Good for API response. """ is_error = 400 <= code < 600 if is_error: webapp.RequestHandler.error(self, code) else: self.response.set_status(code) if not message and not message_html: message = '%d: %s' % (code, httplib.responses.get(code)) if style == 'plain': self.__render_plain_message(message, message_html) else: try: self.render('message.html', cls=style, message=message, message_html=message_html) except: self.__render_plain_message(message, message_html) self.terminate_response() def __render_plain_message(self, message, message_html): self.response.out.write( django.utils.html.escape(message) + ('<p>' if message and message_html else '') + message_html) def terminate_response(self): """Prevents any further output from being written.""" self.response.out.write = lambda *args: None self.get = lambda *args: None self.post = lambda *args: None def write(self, text): """Sends text to the client using the charset from select_charset().""" self.response.out.write(text.encode(self.env.charset, 'replace')) def get_url(self, action, repo=None, scheme=None, **params): """Constructs the absolute URL for a given action and query parameters, preserving the current repo and the parameters listed in PRESERVED_QUERY_PARAM_NAMES.""" return get_url(self.request, repo or self.env.repo, action, charset=self.env.charset, scheme=scheme, **params) @staticmethod def add_task_for_repo(repo, name, action, **kwargs): """Queues up a task for an individual repository.""" task_name = '%s-%s-%s' % (repo, name, int(time.time() * 1000)) path = '/%s/%s' % (repo, action) taskqueue.add(name=task_name, method='GET', url=path, params=kwargs) def send_mail(self, to, subject, body): """Sends e-mail using a sender address that's allowed for this app.""" app_id = get_app_name() sender = 'Do not reply <do-not-reply@%s.%s>' % (app_id, EMAIL_DOMAIN) logging.info('Add mail task: recipient %r, subject %r' % (to, subject)) taskqueue.add(queue_name='send-mail', url='/global/admin/send_mail', params={ 'sender': sender, 'to': to, 'subject': subject, 'body': body }) def get_captcha_html(self, error_code=None, use_ssl=False): """Generates the necessary HTML to display a CAPTCHA validation box.""" # We use the 'custom_translations' parameter for UI messages, whereas # the 'lang' parameter controls the language of the challenge itself. # reCAPTCHA falls back to 'en' if this parameter isn't recognized. lang = self.env.lang.split('-')[0] return captcha.get_display_html( public_key=config.get('captcha_public_key'), use_ssl=use_ssl, error=error_code, lang=lang, custom_translations={ # reCAPTCHA doesn't support all languages, so we treat its # messages as part of this app's usual translation workflow 'instructions_visual': _('Type the two words:'), 'instructions_audio': _('Type what you hear:'), 'play_again': _('Play the sound again'), 'cant_hear_this': _('Download the sound as MP3'), 'visual_challenge': _('Get a visual challenge'), 'audio_challenge': _('Get an audio challenge'), 'refresh_btn': _('Get a new challenge'), 'help_btn': _('Help'), 'incorrect_try_again': _('Incorrect. Try again.') }) def get_captcha_response(self): """Returns an object containing the CAPTCHA response information for the given request's CAPTCHA field information.""" challenge = self.request.get('recaptcha_challenge_field') response = self.request.get('recaptcha_response_field') remote_ip = os.environ['REMOTE_ADDR'] return captcha.submit(challenge, response, config.get('captcha_private_key'), remote_ip) def handle_exception(self, exception, debug_mode): logging.error(traceback.format_exc()) self.error( 500, _('There was an error processing your request. Sorry for the ' 'inconvenience. Our administrators will investigate the source ' 'of the problem, but please check that the format of your ' 'request is correct.')) def to_local_time(self, date): """Converts a datetime object to the local time configured for the current repository. For convenience, returns None if date is None.""" # TODO(kpy): This only works for repositories that have a single fixed # time zone offset and never use Daylight Saving Time. if date: if self.config.time_zone_offset: return date + timedelta(0, 3600 * self.config.time_zone_offset) return date def __init__(self, request, response, env): webapp.RequestHandler.__init__(self, request, response) self.params = Struct() self.env = env self.repo = env.repo self.config = env.config self.charset = env.charset # Set default Content-Type header. self.response.headers['Content-Type'] = ('text/html; charset=%s' % self.charset) # Validate query parameters. for name, validator in self.auto_params.items(): try: value = self.request.get(name, '') setattr(self.params, name, validator(value)) except Exception, e: setattr(self.params, name, validator(None)) return self.error(400, 'Invalid parameter %s: %s' % (name, e)) # Ensure referrer is in whitelist, if it exists if self.params.referrer and (not self.params.referrer in self.config.referrer_whitelist): setattr(self.params, 'referrer', '') # Log the User-Agent header. sample_rate = float(self.config and self.config.user_agent_sample_rate or 0) if random.random() < sample_rate: model.UserAgentLog( repo=self.repo, sample_rate=sample_rate, user_agent=self.request.headers.get('User-Agent'), lang=lang, accept_charset=self.request.headers.get('Accept-Charset', ''), ip_address=self.request.remote_addr).put() # Check for SSL (unless running on localhost for development). if self.https_required and self.env.domain != 'localhost': if self.env.scheme != 'https': return self.error(403, 'HTTPS is required.') # Check for an authorization key. self.auth = None if self.params.key: if self.repo: # check for domain specific one. self.auth = model.Authorization.get(self.repo, self.params.key) if not self.auth: # perhaps this is a global key ('*' for consistency with config). self.auth = model.Authorization.get('*', self.params.key) if self.auth and not self.auth.is_valid: self.auth = None # Handlers that don't need a repository configuration can skip it. if not self.repo: if self.repo_required: return self.error(400, 'No repository specified.') return # Everything after this requires a repo. # Reject requests for repositories that don't exist. if not model.Repo.get_by_key_name(self.repo): if legacy_redirect.do_redirect(self): return legacy_redirect.redirect(self) html = 'No such repository. ' if self.env.repo_options: html += 'Select:<p>' + self.render_to_string('repo-menu.html') return self.error(404, message_html=html) # If this repository has been deactivated, terminate with a message. # The ignore_deactivation flag is for admin pages that bypass this. if self.config.deactivated and not self.ignore_deactivation: self.env.language_menu = [] self.env.robots_ok = True self.render('message.html', cls='deactivation', message_html=self.config.deactivation_message_html) self.terminate_response()