1#!/usr/bin/python
2# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import cgi
7import logging
8import re
9import os
10
11from google.appengine.ext import webapp
12from google.appengine.ext.webapp.util import run_wsgi_app
13from google.appengine.api import memcache
14from google.appengine.api import urlfetch
15
16# TODO(nickbaum): unit tests
17
18
19# TODO(nickbaum): is this the right way to do constants?
20class Channel():
21  def __init__(self, name, tag):
22    self.name = name
23    self.tag = tag
24
25  # TODO(nickbaum): unit test this
26  def matchPath(self, path):
27    match = "/" + self.name + "/"
28    if path[0:len(match)] == match:
29      return true
30    else:
31      return false
32
33Channel.DEV = Channel("dev", "2.0-dev")
34Channel.BETA = Channel("beta", "1.1-beta")
35Channel.STABLE = Channel("stable", "")
36Channel.CHANNELS = [Channel.DEV, Channel.BETA, Channel.STABLE]
37Channel.TRUNK = Channel("trunk", "")
38Channel.DEFAULT = Channel.STABLE
39
40
41DEFAULT_CACHE_TIME = 300
42
43
44class MainPage(webapp.RequestHandler):
45  # get page from memcache, or else fetch it from src
46  def get(self):
47    path = os.path.realpath(os.path.join('/', self.request.path))
48    # special path to invoke the unit tests
49    # TODO(nickbaum): is there a less ghetto way to invoke the unit test?
50    if path == "/test":
51      self.unitTest()
52      return
53    # if root, redirect to index.html
54    # TODO(nickbaum): this doesn't handle /chrome/extensions/trunk, etc
55    if (path == "/chrome/extensions") or (path == "chrome/extensions/"):
56      self.redirect("/chrome/extensions/index.html")
57      return
58    # else remove prefix
59    if(path[:18] == "/chrome/extensions"):
60      path = path[18:]
61    # TODO(nickbaum): there's a subtle bug here: if there are two instances of the app,
62    # their default caches will override each other. This is bad!
63    result = memcache.get(path)
64    if result is None:
65      logging.info("Cache miss: " + path)
66      url = self.getSrcUrl(path)
67      if (url[1] is not Channel.TRUNK) and (url[0] != "http://src.chromium.org/favicon.ico"):
68        branch = self.getBranch(url[1])
69        url = url[0] % branch
70      else:
71        url = url[0]
72      logging.info("Path: " + self.request.path)
73      logging.info("Url: " + url)
74      try:
75        result = urlfetch.fetch(url + self.request.query_string)
76        if result.status_code != 200:
77          logging.error("urlfetch failed: " + url)
78          # TODO(nickbaum): what should we do when the urlfetch fails?
79      except:
80        logging.error("urlfetch failed: " + url)
81        # TODO(nickbaum): what should we do when the urlfetch fails?
82      try:
83        if not memcache.add(path, result, DEFAULT_CACHE_TIME):
84          logging.error("Memcache set failed.")
85      except:
86        logging.error("Memcache set failed.")
87    for key in result.headers:
88      self.response.headers[key] = result.headers[key]
89    self.response.out.write(result.content)
90
91  def head(self):
92    self.get()
93
94  # get the src url corresponding to the request
95  # returns a tuple of the url and the branch
96  # this function is the only part that is unit tested
97  def getSrcUrl(self, path):
98    # from the path they provided, figure out which channel they requested
99    # TODO(nickbaum) clean this logic up
100    # find the first subdirectory of the path
101    path = path.split('/', 2)
102    url = "http://src.chromium.org/viewvc/chrome/"
103    channel = None
104    # if there's no subdirectory, choose the default channel
105    # otherwise, figure out if the subdirectory corresponds to a channel
106    if len(path) == 2:
107      path.append("")
108    if path[1] == "":
109      channel = Channel.DEFAULT
110      if(Channel.DEFAULT == Channel.TRUNK):
111        url = url + "trunk/src/chrome/"
112      else:
113        url = url + "branches/%s/src/chrome/"
114      path = ""
115    elif path[1] == Channel.TRUNK.name:
116      url = url + "trunk/src/chrome/"
117      channel = Channel.TRUNK
118      path = path[2]
119    else:
120      # otherwise, run through the different channel options
121      for c in Channel.CHANNELS:
122        if(path[1] == c.name):
123          channel = c
124          url = url + "branches/%s/src/chrome/"
125          path = path[2]
126          break
127      # if the subdirectory doesn't correspond to a channel, use the default
128      if channel is None:
129        channel = Channel.DEFAULT
130        if(Channel.DEFAULT == Channel.TRUNK):
131          url = url + "trunk/src/chrome/"
132        else:
133          url = url + "branches/%s/src/chrome/"
134        if path[2] != "":
135          path = path[1] + "/" + path[2]
136        else:
137          path = path[1]
138    # special cases
139    # TODO(nickbaum): this is super cumbersome to maintain
140    if path == "third_party/jstemplate/jstemplate_compiled.js":
141      url = url + path
142    elif path == "api/extension_api.json":
143      url = url + "common/extensions/" + path
144    elif path == "favicon.ico":
145      url = "http://src.chromium.org/favicon.ico"
146    else:
147      if path == "":
148        path = "index.html"
149      url = url + "common/extensions/docs/" + path
150    return [url, channel]
151
152  # get the current version number for the channel requested (dev, beta or stable)
153  # TODO(nickbaum): move to Channel object
154  def getBranch(self, channel):
155    branch = memcache.get(channel.name)
156    if branch is None:
157      # query Omaha to figure out which version corresponds to this channel
158      postdata = """<?xml version="1.0" encoding="UTF-8"?>
159                    <o:gupdate xmlns:o="http://www.google.com/update2/request" protocol="2.0" testsource="crxdocs">
160                    <o:app appid="{8A69D345-D564-463C-AFF1-A69D9E530F96}" version="0.0.0.0" lang="">
161                    <o:updatecheck tag="%s" installsource="ondemandcheckforupdates" />
162                    </o:app>
163                    </o:gupdate>
164                    """ % channel.tag
165      result = urlfetch.fetch(url="https://tools.google.com/service/update2",
166                        payload=postdata,
167                        method=urlfetch.POST,
168                        headers={'Content-Type': 'application/x-www-form-urlencoded',
169                                 'X-USER-IP': '72.1.1.1'})
170      if result.status_code != 200:
171        logging.error("urlfetch failed.")
172        # TODO(nickbaum): what should we do when the urlfetch fails?
173      # find branch in response
174      match = re.search(r'<updatecheck Version="\d+\.\d+\.(\d+)\.\d+"', result.content)
175      if match is None:
176        logging.error("Version number not found: " + result.content)
177        #TODO(nickbaum): should we fall back on trunk in this case?
178      branch = match.group(1)
179      # TODO(nickbaum): make cache time a constant
180      if not memcache.add(channel.name, branch, DEFAULT_CACHE_TIME):
181        logging.error("Memcache set failed.")
182    return branch
183
184  # TODO(nickbaum): is there a more elegant way to write this unit test?
185  # I deliberately kept it dumb to avoid errors sneaking in, but it's so verbose...
186  # TODO(nickbaum): should I break this up into multiple files?
187  def unitTest(self):
188    self.response.out.write("Testing TRUNK<br/>")
189    self.check("/trunk/", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK)
190    self.check("/trunk/index.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK)
191    self.check("/trunk/getstarted.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/getstarted.html", Channel.TRUNK)
192    self.check("/trunk/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.TRUNK)
193
194    self.response.out.write("<br/>Testing DEV<br/>")
195    self.check("/dev/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV)
196    self.check("/dev/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV)
197    self.check("/dev/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.DEV)
198    self.check("/dev/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.DEV)
199
200    self.response.out.write("<br/>Testing BETA<br/>")
201    self.check("/beta/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.BETA)
202    self.check("/beta/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.BETA)
203    self.check("/beta/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.BETA)
204    self.check("/beta/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.BETA)
205
206    self.response.out.write("<br/>Testing STABLE<br/>")
207    self.check("/stable/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.STABLE)
208    self.check("/stable/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.STABLE)
209    self.check("/stable/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.STABLE)
210    self.check("/stable/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.STABLE)
211
212    self.response.out.write("<br/>Testing jstemplate_compiled.js<br/>")
213    self.check("/trunk/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.TRUNK)
214    self.check("/dev/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.DEV)
215    self.check("/beta/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.BETA)
216    self.check("/stable/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.STABLE)
217
218    self.response.out.write("<br/>Testing extension_api.json<br/>")
219    self.check("/trunk/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/api/extension_api.json", Channel.TRUNK)
220    self.check("/dev/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.DEV)
221    self.check("/beta/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.BETA)
222    self.check("/stable/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.STABLE)
223
224    self.response.out.write("<br/>Testing favicon.ico<br/>")
225    self.check("/trunk/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.TRUNK)
226    self.check("/dev/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.DEV)
227    self.check("/beta/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.BETA)
228    self.check("/stable/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.STABLE)
229
230    self.response.out.write("<br/>Testing DEFAULT<br/>")
231    temp = Channel.DEFAULT
232    Channel.DEFAULT = Channel.DEV
233    self.check("/", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV)
234    self.check("/index.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/index.html", Channel.DEV)
235    self.check("/getstarted.html", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/getstarted.html", Channel.DEV)
236    self.check("/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.DEV)
237    self.check("/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.DEV)
238    self.check("/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/api/extension_api.json", Channel.DEV)
239    self.check("/css/ApiRefStyles.css", "http://src.chromium.org/viewvc/chrome/branches/%s/src/chrome/common/extensions/docs/css/ApiRefStyles.css", Channel.DEV)
240    self.check("/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.DEV)
241
242    self.response.out.write("<br/>Testing DEFAULT (trunk)<br/>")
243    Channel.DEFAULT = Channel.TRUNK
244    self.check("/", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK)
245    self.check("/index.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/index.html", Channel.TRUNK)
246    self.check("/getstarted.html", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/getstarted.html", Channel.TRUNK)
247    self.check("/images/toolstrip.png", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/images/toolstrip.png", Channel.TRUNK)
248    self.check("/third_party/jstemplate/jstemplate_compiled.js", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/third_party/jstemplate/jstemplate_compiled.js", Channel.TRUNK)
249    self.check("/api/extension_api.json", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/api/extension_api.json", Channel.TRUNK)
250    self.check("/css/ApiRefStyles.css", "http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/css/ApiRefStyles.css", Channel.TRUNK)
251    self.check("/favicon.ico", "http://src.chromium.org/favicon.ico", Channel.TRUNK)
252    Channel.DEFAULT = temp
253
254    return
255
256  # utility function for my unit test
257  # checks that getSrcUrl(path) returns the expected values
258  # TODO(nickbaum): can this be replaced by assert or something similar?
259  def check(self, path, expectedUrl, expectedChannel):
260    actual = self.getSrcUrl(path)
261    if (actual[0] != expectedUrl):
262      self.response.out.write('<span style="color:#f00;">Failure:</span> path ' + path + " gave url " + actual[0] + "<br/>")
263    elif (actual[1] != expectedChannel):
264      self.response.out.write('<span style="color:#f00;">Failure:</span> path ' + path + " gave branch " + actual[1].name + "<br/>")
265    else:
266      self.response.out.write("Path " + path + ' <span style="color:#0f0;">OK</span><br/>')
267    return
268
269
270application = webapp.WSGIApplication([
271  ('/.*', MainPage),
272], debug=False)
273
274
275def main():
276  run_wsgi_app(application)
277
278
279if __name__ == '__main__':
280  main()
281