blob: 44e9cea72d757fd5284d27b47a622cd608665f35 [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"""This file defines a subclass of Servlet for JSON feeds.
7
8A "feed" is a servlet that is accessed by another part of our system and that
9responds with a JSON value rather than HTML to display in a browser.
10"""
11from __future__ import print_function
12from __future__ import division
13from __future__ import absolute_import
14
15import httplib
16import json
17import logging
18
19from google.appengine.api import app_identity
20
21import settings
22
23from framework import framework_constants
24from framework import permissions
25from framework import servlet
26from 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 """
52 raise servlet.MethodNotSupportedError()
53
54 def _DoRequestHandling(self, request, mr):
55 """Do permission checking, page processing, and response formatting."""
56 try:
57 # TODO(jrobbins): check the XSRF token even for anon users
58 # after the next deployment.
59 if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
60 # Validate the XSRF token with the specific request path for this
61 # servlet. But, not every XHR request has a distinct token, so just
62 # use 'xhr' for ones that don't.
63 # TODO(jrobbins): make specific tokens for:
64 # user and project stars, issue options, check names.
65 try:
66 logging.info('request in jsonfeed is %r', request)
67 xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path)
68 except xsrf.TokenIncorrect:
69 logging.info('using token path "xhr"')
70 xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
71
72 if self.CHECK_SAME_APP and not settings.local_mode:
73 calling_app_id = request.headers.get('X-Appengine-Inbound-Appid')
74 if calling_app_id != app_identity.get_application_id():
75 self.response.status = httplib.FORBIDDEN
76 return
77
78 self._CheckForMovedProject(mr, request)
79 self.AssertBasePermission(mr)
80
81 json_data = self.HandleRequest(mr)
82
83 self._RenderJsonResponse(json_data)
84
85 except query2ast.InvalidQueryError as e:
86 logging.warning('Trapped InvalidQueryError: %s', e)
87 logging.exception(e)
88 msg = e.message if e.message else 'invalid query'
89 self.abort(400, msg)
90 except permissions.PermissionException as e:
91 logging.info('Trapped PermissionException %s', e)
92 self.response.status = httplib.FORBIDDEN
93
94 # pylint: disable=unused-argument
95 # pylint: disable=arguments-differ
96 # Note: unused arguments necessary because they are specified in
97 # registerpages.py as an extra URL validation step even though we
98 # do our own URL parsing in monorailrequest.py
99 def get(self, project_name=None, viewed_username=None, hotlist_id=None):
100 """Collect page-specific and generic info, then render the page.
101
102 Args:
103 project_name: string project name parsed from the URL by webapp2,
104 but we also parse it out in our code.
105 viewed_username: string user email parsed from the URL by webapp2,
106 but we also parse it out in our code.
107 hotlist_id: string hotlist id parsed from the URL by webapp2,
108 but we also parse it out in our code.
109 """
110 self._DoRequestHandling(self.mr.request, self.mr)
111
112 # pylint: disable=unused-argument
113 # pylint: disable=arguments-differ
114 def post(self, project_name=None, viewed_username=None, hotlist_id=None):
115 """Parse the request, check base perms, and call form-specific code."""
116 self._DoRequestHandling(self.mr.request, self.mr)
117
118 def _RenderJsonResponse(self, json_data):
119 """Serialize the data as JSON so that it can be sent to the browser."""
120 json_str = json.dumps(json_data, indent=self.JSON_INDENT)
121 logging.debug(
122 'Sending JSON response: %r length: %r',
123 json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str))
124 self.response.content_type = framework_constants.CONTENT_TYPE_JSON
125 self.response.headers['X-Content-Type-Options'] = (
126 framework_constants.CONTENT_TYPE_JSON_OPTIONS)
127 self.response.write(XSSI_PREFIX)
128 self.response.write(json_str)
129
130
131class InternalTask(JsonFeed):
132 """Internal tasks are JSON feeds that can only be reached by our own code."""
133
134 CHECK_SECURITY_TOKEN = False