blob: b357fd80b5fc9fa17083fe6938ce9bbbbb00fc0e [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"""This file defines a subclass of Servlet for JSON feeds.
6
7A "feed" is a servlet that is accessed by another part of our system and that
8responds with a JSON value rather than HTML to display in a browser.
9"""
10from __future__ import print_function
11from __future__ import division
12from __future__ import absolute_import
13
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020014from six.moves import http_client
Copybara854996b2021-09-07 19:36:02 +000015import json
16import logging
17
18from google.appengine.api import app_identity
19
20import settings
21
22from framework import framework_constants
23from framework import permissions
24from framework import servlet
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010025from framework import servlet_helpers
Copybara854996b2021-09-07 19:36:02 +000026from framework import xsrf
27from search import query2ast
28
29# This causes a JS error for a hacker trying to do a cross-site inclusion.
30XSSI_PREFIX = ")]}'\n"
31
32
33class JsonFeed(servlet.Servlet):
34 """A convenient base class for JSON feeds."""
35
36 # By default, JSON output is compact. Subclasses can set this to
37 # an integer, like 4, for pretty-printed output.
38 JSON_INDENT = None
39
40 # Some JSON handlers can only be accessed from our own app.
41 CHECK_SAME_APP = False
42
43 def HandleRequest(self, _mr):
44 """Override this method to implement handling of the request.
45
46 Args:
47 mr: common information parsed from the HTTP request.
48
49 Returns:
50 A dictionary of json data.
51 """
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020052 raise servlet_helpers.MethodNotSupportedError()
Copybara854996b2021-09-07 19:36:02 +000053
54 def _DoRequestHandling(self, request, mr):
55 """Do permission checking, page processing, and response formatting."""
56 try:
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020057 if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
58 try:
59 logging.info('request in jsonfeed is %r', request)
60 xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path)
61 except xsrf.TokenIncorrect:
62 logging.info('using token path "xhr"')
63 xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
64
65 if self.CHECK_SAME_APP and not settings.local_mode:
66 calling_app_id = request.headers.get('X-Appengine-Inbound-Appid')
67 if calling_app_id != app_identity.get_application_id():
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020068 self.response.status_code = http_client.FORBIDDEN
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020069 return
70
71 self._CheckForMovedProject(mr, request)
72 self.AssertBasePermission(mr)
73
74 json_data = self.HandleRequest(mr)
75
76 self._RenderJsonResponse(json_data)
77
78 except query2ast.InvalidQueryError as e:
79 logging.warning('Trapped InvalidQueryError: %s', e)
80 logging.exception(e)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010081 msg = str(e) if str(e) else 'invalid query'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020082 self.abort(400, msg)
83 except permissions.PermissionException as e:
84 logging.info('Trapped PermissionException %s', e)
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020085 self.response.status_code = http_client.FORBIDDEN
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020086
87 # pylint: disable=unused-argument
88 # pylint: disable=arguments-differ
89 # Note: unused arguments necessary because they are specified in
90 # registerpages.py as an extra URL validation step even though we
91 # do our own URL parsing in monorailrequest.py
92 def get(self, **kwargs):
93 """Collect page-specific and generic info, then render the page.
94
95 Args:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010096 project_name: string project name parsed from the URL by Flask,
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020097 but we also parse it out in our code.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010098 viewed_username: string user email parsed from the URL by Flask,
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020099 but we also parse it out in our code.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100100 hotlist_id: string hotlist id parsed from the URL by Flask,
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200101 but we also parse it out in our code.
102 """
103 self._DoRequestHandling(self.mr.request, self.mr)
104
105 # pylint: disable=unused-argument
106 # pylint: disable=arguments-differ
107 def post(self, **kwargs):
108 """Parse the request, check base perms, and call form-specific code."""
109 self._DoRequestHandling(self.mr.request, self.mr)
110
111 def _RenderJsonResponse(self, json_data):
112 """Serialize the data as JSON so that it can be sent to the browser."""
113 json_str = json.dumps(json_data, indent=self.JSON_INDENT)
114 logging.debug(
115 'Sending JSON response: %r length: %r',
116 json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str))
117 self.response.content_type = framework_constants.CONTENT_TYPE_JSON
118 self.response.headers['X-Content-Type-Options'] = (
119 framework_constants.CONTENT_TYPE_JSON_OPTIONS)
120 self.response.set_data(XSSI_PREFIX + json_str)
121
122
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100123class InternalTask(JsonFeed):
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200124 """Internal tasks are JSON feeds that can only be reached by our own code."""
125
126 CHECK_SECURITY_TOKEN = False