Index: appengine/monorail/framework/jsonfeed.py |
diff --git a/appengine/monorail/framework/jsonfeed.py b/appengine/monorail/framework/jsonfeed.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a2cb6d568c44c6952d2eef08528494352d257bb8 |
--- /dev/null |
+++ b/appengine/monorail/framework/jsonfeed.py |
@@ -0,0 +1,117 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered by a BSD-style |
+# license that can be found in the LICENSE file or at |
+# https://developers.google.com/open-source/licenses/bsd |
+ |
+"""This file defines a subclass of Servlet for JSON feeds. |
+ |
+A "feed" is a servlet that is accessed by another part of our system and that |
+responds with a JSON value rather than HTML to display in a browser. |
+""" |
+ |
+import httplib |
+import json |
+import logging |
+ |
+from google.appengine.api import app_identity |
+ |
+import settings |
+ |
+from framework import framework_constants |
+from framework import permissions |
+from framework import servlet |
+from framework import xsrf |
+ |
+# This causes a JS error for a hacker trying to do a cross-site inclusion. |
+XSSI_PREFIX = ")]}'\n" |
+ |
+ |
+class JsonFeed(servlet.Servlet): |
+ """A convenient base class for JSON feeds.""" |
+ |
+ # By default, JSON output is compact. Subclasses can set this to |
+ # an integer, like 4, for pretty-printed output. |
+ JSON_INDENT = None |
+ |
+ # Some JSON handlers can only be accessed from our own app. |
+ CHECK_SAME_APP = False |
+ |
+ def HandleRequest(self, _mr): |
+ """Override this method to implement handling of the request. |
+ |
+ Args: |
+ mr: common information parsed from the HTTP request. |
+ |
+ Returns: |
+ A dictionary of json data. |
+ """ |
+ raise servlet.MethodNotSupportedError() |
+ |
+ def _DoRequestHandling(self, request, mr): |
+ """Do permission checking, page processing, and response formatting.""" |
+ try: |
+ if self.CHECK_SECURITY_TOKEN and mr.auth.user_id: |
+ # Validate the XSRF token with the specific request path for this |
+ # servlet. But, not every XHR request has a distinct token, so just |
+ # use 'xhr' for ones that don't. |
+ # TODO(jrobbins): make specific tokens for: |
+ # user and project stars, issue options, check names. |
+ try: |
+ logging.info('request in jsonfeed is %r', request) |
+ xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path) |
+ except xsrf.TokenIncorrect: |
+ logging.info('using token path "xhr"') |
+ xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH) |
+ |
+ if self.CHECK_SAME_APP and not settings.dev_mode: |
+ calling_app_id = request.headers.get('X-Appengine-Inbound-Appid') |
+ if calling_app_id != app_identity.get_application_id(): |
+ self.response.status = httplib.FORBIDDEN |
+ return |
+ |
+ self._CheckForMovedProject(mr, request) |
+ self.AssertBasePermission(mr) |
+ |
+ json_data = self.HandleRequest(mr) |
+ |
+ self._RenderJsonResponse(json_data) |
+ raise servlet.AlreadySentResponseException() |
+ |
+ except permissions.PermissionException as e: |
+ logging.info('Trapped PermissionException %s', e) |
+ self.response.status = httplib.FORBIDDEN |
+ |
+ # pylint: disable=unused-argument |
+ # pylint: disable=arguments-differ |
+ def get(self, project_name=None, viewed_username=None): |
+ """Collect page-specific and generic info, then render the page. |
+ |
+ Args: |
+ project_name: string project name parsed from the URL by webapp2, |
+ but we also parse it out in our code. |
+ viewed_username: string user email parsed from the URL by webapp2, |
+ but we also parse it out in our code. |
+ """ |
+ self._DoRequestHandling(self.mr.request, self.mr) |
+ |
+ # pylint: disable=unused-argument |
+ # pylint: disable=arguments-differ |
+ def post(self, project_name=None, viewed_username=None): |
+ """Parse the request, check base perms, and call form-specific code.""" |
+ self._DoRequestHandling(self.mr.request, self.mr) |
+ |
+ def _RenderJsonResponse(self, json_data): |
+ """Serialize the data as JSON so that it can be sent to the browser.""" |
+ json_str = json.dumps(json_data, indent=self.JSON_INDENT) |
+ logging.debug( |
+ 'Sending JSON response: %r length: %r', |
+ json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str)) |
+ self.response.content_type = framework_constants.CONTENT_TYPE_JSON |
+ self.response.write(XSSI_PREFIX) |
+ self.response.write(json_str) |
+ |
+ |
+class InternalTask(JsonFeed): |
+ """Internal tasks are JSON feeds that can only be reached by our own code.""" |
+ |
+ CHECK_SECURITY_TOKEN = False |