##// END OF EJS Templates
hgweb: don't access self.repo during request processing...
Gregory Szorc -
r26209:7917746c default
parent child Browse files
Show More
@@ -1,447 +1,447
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial.templatefilters import websub
11 from mercurial.templatefilters import websub
12 from common import get_stat, ErrorResponse, permhooks, caching
12 from common import get_stat, ErrorResponse, permhooks, caching
13 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from request import wsgirequest
15 from request import wsgirequest
16 import webcommands, protocol, webutil
16 import webcommands, protocol, webutil
17
17
18 perms = {
18 perms = {
19 'changegroup': 'pull',
19 'changegroup': 'pull',
20 'changegroupsubset': 'pull',
20 'changegroupsubset': 'pull',
21 'getbundle': 'pull',
21 'getbundle': 'pull',
22 'stream_out': 'pull',
22 'stream_out': 'pull',
23 'listkeys': 'pull',
23 'listkeys': 'pull',
24 'unbundle': 'push',
24 'unbundle': 'push',
25 'pushkey': 'push',
25 'pushkey': 'push',
26 }
26 }
27
27
28 ## Files of interest
28 ## Files of interest
29 # Used to check if the repository has changed looking at mtime and size of
29 # Used to check if the repository has changed looking at mtime and size of
30 # theses files. This should probably be relocated a bit higher in core.
30 # theses files. This should probably be relocated a bit higher in core.
31 foi = [('spath', '00changelog.i'),
31 foi = [('spath', '00changelog.i'),
32 ('spath', 'phaseroots'), # ! phase can change content at the same size
32 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 ('spath', 'obsstore'),
33 ('spath', 'obsstore'),
34 ('path', 'bookmarks'), # ! bookmark can change content at the same size
34 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 ]
35 ]
36
36
37 def makebreadcrumb(url, prefix=''):
37 def makebreadcrumb(url, prefix=''):
38 '''Return a 'URL breadcrumb' list
38 '''Return a 'URL breadcrumb' list
39
39
40 A 'URL breadcrumb' is a list of URL-name pairs,
40 A 'URL breadcrumb' is a list of URL-name pairs,
41 corresponding to each of the path items on a URL.
41 corresponding to each of the path items on a URL.
42 This can be used to create path navigation entries.
42 This can be used to create path navigation entries.
43 '''
43 '''
44 if url.endswith('/'):
44 if url.endswith('/'):
45 url = url[:-1]
45 url = url[:-1]
46 if prefix:
46 if prefix:
47 url = '/' + prefix + url
47 url = '/' + prefix + url
48 relpath = url
48 relpath = url
49 if relpath.startswith('/'):
49 if relpath.startswith('/'):
50 relpath = relpath[1:]
50 relpath = relpath[1:]
51
51
52 breadcrumb = []
52 breadcrumb = []
53 urlel = url
53 urlel = url
54 pathitems = [''] + relpath.split('/')
54 pathitems = [''] + relpath.split('/')
55 for pathel in reversed(pathitems):
55 for pathel in reversed(pathitems):
56 if not pathel or not urlel:
56 if not pathel or not urlel:
57 break
57 break
58 breadcrumb.append({'url': urlel, 'name': pathel})
58 breadcrumb.append({'url': urlel, 'name': pathel})
59 urlel = os.path.dirname(urlel)
59 urlel = os.path.dirname(urlel)
60 return reversed(breadcrumb)
60 return reversed(breadcrumb)
61
61
62 class requestcontext(object):
62 class requestcontext(object):
63 """Holds state/context for an individual request.
63 """Holds state/context for an individual request.
64
64
65 Servers can be multi-threaded. Holding state on the WSGI application
65 Servers can be multi-threaded. Holding state on the WSGI application
66 is prone to race conditions. Instances of this class exist to hold
66 is prone to race conditions. Instances of this class exist to hold
67 mutable and race-free state for requests.
67 mutable and race-free state for requests.
68 """
68 """
69 def __init__(self, app):
69 def __init__(self, app):
70 object.__setattr__(self, 'app', app)
70 object.__setattr__(self, 'app', app)
71 object.__setattr__(self, 'repo', app.repo)
71 object.__setattr__(self, 'repo', app.repo)
72
72
73 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
73 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
74
74
75 object.__setattr__(self, 'maxchanges',
75 object.__setattr__(self, 'maxchanges',
76 self.configint('web', 'maxchanges', 10))
76 self.configint('web', 'maxchanges', 10))
77 object.__setattr__(self, 'stripecount',
77 object.__setattr__(self, 'stripecount',
78 self.configint('web', 'stripes', 1))
78 self.configint('web', 'stripes', 1))
79 object.__setattr__(self, 'maxshortchanges',
79 object.__setattr__(self, 'maxshortchanges',
80 self.configint('web', 'maxshortchanges', 60))
80 self.configint('web', 'maxshortchanges', 60))
81 object.__setattr__(self, 'maxfiles',
81 object.__setattr__(self, 'maxfiles',
82 self.configint('web', 'maxfiles', 10))
82 self.configint('web', 'maxfiles', 10))
83 object.__setattr__(self, 'allowpull',
83 object.__setattr__(self, 'allowpull',
84 self.configbool('web', 'allowpull', True))
84 self.configbool('web', 'allowpull', True))
85
85
86 # we use untrusted=False to prevent a repo owner from using
86 # we use untrusted=False to prevent a repo owner from using
87 # web.templates in .hg/hgrc to get access to any file readable
87 # web.templates in .hg/hgrc to get access to any file readable
88 # by the user running the CGI script
88 # by the user running the CGI script
89 object.__setattr__(self, 'templatepath',
89 object.__setattr__(self, 'templatepath',
90 self.config('web', 'templates', untrusted=False))
90 self.config('web', 'templates', untrusted=False))
91
91
92 # This object is more expensive to build than simple config values.
92 # This object is more expensive to build than simple config values.
93 # It is shared across requests. The app will replace the object
93 # It is shared across requests. The app will replace the object
94 # if it is updated. Since this is a reference and nothing should
94 # if it is updated. Since this is a reference and nothing should
95 # modify the underlying object, it should be constant for the lifetime
95 # modify the underlying object, it should be constant for the lifetime
96 # of the request.
96 # of the request.
97 object.__setattr__(self, 'websubtable', app.websubtable)
97 object.__setattr__(self, 'websubtable', app.websubtable)
98
98
99 # Proxy unknown reads and writes to the application instance
99 # Proxy unknown reads and writes to the application instance
100 # until everything is moved to us.
100 # until everything is moved to us.
101 def __getattr__(self, name):
101 def __getattr__(self, name):
102 return getattr(self.app, name)
102 return getattr(self.app, name)
103
103
104 def __setattr__(self, name, value):
104 def __setattr__(self, name, value):
105 return setattr(self.app, name, value)
105 return setattr(self.app, name, value)
106
106
107 # Servers are often run by a user different from the repo owner.
107 # Servers are often run by a user different from the repo owner.
108 # Trust the settings from the .hg/hgrc files by default.
108 # Trust the settings from the .hg/hgrc files by default.
109 def config(self, section, name, default=None, untrusted=True):
109 def config(self, section, name, default=None, untrusted=True):
110 return self.repo.ui.config(section, name, default,
110 return self.repo.ui.config(section, name, default,
111 untrusted=untrusted)
111 untrusted=untrusted)
112
112
113 def configbool(self, section, name, default=False, untrusted=True):
113 def configbool(self, section, name, default=False, untrusted=True):
114 return self.repo.ui.configbool(section, name, default,
114 return self.repo.ui.configbool(section, name, default,
115 untrusted=untrusted)
115 untrusted=untrusted)
116
116
117 def configint(self, section, name, default=None, untrusted=True):
117 def configint(self, section, name, default=None, untrusted=True):
118 return self.repo.ui.configint(section, name, default,
118 return self.repo.ui.configint(section, name, default,
119 untrusted=untrusted)
119 untrusted=untrusted)
120
120
121 def configlist(self, section, name, default=None, untrusted=True):
121 def configlist(self, section, name, default=None, untrusted=True):
122 return self.repo.ui.configlist(section, name, default,
122 return self.repo.ui.configlist(section, name, default,
123 untrusted=untrusted)
123 untrusted=untrusted)
124
124
125 archivespecs = {
125 archivespecs = {
126 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
126 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
127 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
127 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
128 'zip': ('application/zip', 'zip', '.zip', None),
128 'zip': ('application/zip', 'zip', '.zip', None),
129 }
129 }
130
130
131 def archivelist(self, nodeid):
131 def archivelist(self, nodeid):
132 allowed = self.configlist('web', 'allow_archive')
132 allowed = self.configlist('web', 'allow_archive')
133 for typ, spec in self.archivespecs.iteritems():
133 for typ, spec in self.archivespecs.iteritems():
134 if typ in allowed or self.configbool('web', 'allow%s' % typ):
134 if typ in allowed or self.configbool('web', 'allow%s' % typ):
135 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
135 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
136
136
137 def templater(self, req):
137 def templater(self, req):
138 # determine scheme, port and server name
138 # determine scheme, port and server name
139 # this is needed to create absolute urls
139 # this is needed to create absolute urls
140
140
141 proto = req.env.get('wsgi.url_scheme')
141 proto = req.env.get('wsgi.url_scheme')
142 if proto == 'https':
142 if proto == 'https':
143 proto = 'https'
143 proto = 'https'
144 default_port = '443'
144 default_port = '443'
145 else:
145 else:
146 proto = 'http'
146 proto = 'http'
147 default_port = '80'
147 default_port = '80'
148
148
149 port = req.env['SERVER_PORT']
149 port = req.env['SERVER_PORT']
150 port = port != default_port and (':' + port) or ''
150 port = port != default_port and (':' + port) or ''
151 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
151 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
152 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
152 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
153 logoimg = self.config('web', 'logoimg', 'hglogo.png')
153 logoimg = self.config('web', 'logoimg', 'hglogo.png')
154 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
154 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
155 if not staticurl.endswith('/'):
155 if not staticurl.endswith('/'):
156 staticurl += '/'
156 staticurl += '/'
157
157
158 # some functions for the templater
158 # some functions for the templater
159
159
160 def motd(**map):
160 def motd(**map):
161 yield self.config('web', 'motd', '')
161 yield self.config('web', 'motd', '')
162
162
163 # figure out which style to use
163 # figure out which style to use
164
164
165 vars = {}
165 vars = {}
166 styles = (
166 styles = (
167 req.form.get('style', [None])[0],
167 req.form.get('style', [None])[0],
168 self.config('web', 'style'),
168 self.config('web', 'style'),
169 'paper',
169 'paper',
170 )
170 )
171 style, mapfile = templater.stylemap(styles, self.templatepath)
171 style, mapfile = templater.stylemap(styles, self.templatepath)
172 if style == styles[0]:
172 if style == styles[0]:
173 vars['style'] = style
173 vars['style'] = style
174
174
175 start = req.url[-1] == '?' and '&' or '?'
175 start = req.url[-1] == '?' and '&' or '?'
176 sessionvars = webutil.sessionvars(vars, start)
176 sessionvars = webutil.sessionvars(vars, start)
177
177
178 if not self.reponame:
178 if not self.reponame:
179 self.reponame = (self.config('web', 'name')
179 self.reponame = (self.config('web', 'name')
180 or req.env.get('REPO_NAME')
180 or req.env.get('REPO_NAME')
181 or req.url.strip('/') or self.repo.root)
181 or req.url.strip('/') or self.repo.root)
182
182
183 def websubfilter(text):
183 def websubfilter(text):
184 return websub(text, self.websubtable)
184 return websub(text, self.websubtable)
185
185
186 # create the templater
186 # create the templater
187
187
188 tmpl = templater.templater(mapfile,
188 tmpl = templater.templater(mapfile,
189 filters={'websub': websubfilter},
189 filters={'websub': websubfilter},
190 defaults={'url': req.url,
190 defaults={'url': req.url,
191 'logourl': logourl,
191 'logourl': logourl,
192 'logoimg': logoimg,
192 'logoimg': logoimg,
193 'staticurl': staticurl,
193 'staticurl': staticurl,
194 'urlbase': urlbase,
194 'urlbase': urlbase,
195 'repo': self.reponame,
195 'repo': self.reponame,
196 'encoding': encoding.encoding,
196 'encoding': encoding.encoding,
197 'motd': motd,
197 'motd': motd,
198 'sessionvars': sessionvars,
198 'sessionvars': sessionvars,
199 'pathdef': makebreadcrumb(req.url),
199 'pathdef': makebreadcrumb(req.url),
200 'style': style,
200 'style': style,
201 })
201 })
202 return tmpl
202 return tmpl
203
203
204
204
205 class hgweb(object):
205 class hgweb(object):
206 """HTTP server for individual repositories.
206 """HTTP server for individual repositories.
207
207
208 Instances of this class serve HTTP responses for a particular
208 Instances of this class serve HTTP responses for a particular
209 repository.
209 repository.
210
210
211 Instances are typically used as WSGI applications.
211 Instances are typically used as WSGI applications.
212
212
213 Some servers are multi-threaded. On these servers, there may
213 Some servers are multi-threaded. On these servers, there may
214 be multiple active threads inside __call__.
214 be multiple active threads inside __call__.
215 """
215 """
216 def __init__(self, repo, name=None, baseui=None):
216 def __init__(self, repo, name=None, baseui=None):
217 if isinstance(repo, str):
217 if isinstance(repo, str):
218 if baseui:
218 if baseui:
219 u = baseui.copy()
219 u = baseui.copy()
220 else:
220 else:
221 u = ui.ui()
221 u = ui.ui()
222 r = hg.repository(u, repo)
222 r = hg.repository(u, repo)
223 else:
223 else:
224 # we trust caller to give us a private copy
224 # we trust caller to give us a private copy
225 r = repo
225 r = repo
226
226
227 r = getwebview(r)
227 r = getwebview(r)
228 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
228 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
229 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
229 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
230 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
230 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
231 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
231 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
232 # displaying bundling progress bar while serving feel wrong and may
232 # displaying bundling progress bar while serving feel wrong and may
233 # break some wsgi implementation.
233 # break some wsgi implementation.
234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 self.repo = r
236 self.repo = r
237 hook.redirect(True)
237 hook.redirect(True)
238 self.repostate = None
238 self.repostate = None
239 self.mtime = -1
239 self.mtime = -1
240 self.reponame = name
240 self.reponame = name
241
241
242 def refresh(self):
242 def refresh(self):
243 repostate = []
243 repostate = []
244 mtime = 0
244 mtime = 0
245 # file of interrests mtime and size
245 # file of interrests mtime and size
246 for meth, fname in foi:
246 for meth, fname in foi:
247 prefix = getattr(self.repo, meth)
247 prefix = getattr(self.repo, meth)
248 st = get_stat(prefix, fname)
248 st = get_stat(prefix, fname)
249 repostate.append((st.st_mtime, st.st_size))
249 repostate.append((st.st_mtime, st.st_size))
250 mtime = max(mtime, st.st_mtime)
250 mtime = max(mtime, st.st_mtime)
251 repostate = tuple(repostate)
251 repostate = tuple(repostate)
252 # we need to compare file size in addition to mtime to catch
252 # we need to compare file size in addition to mtime to catch
253 # changes made less than a second ago
253 # changes made less than a second ago
254 if repostate != self.repostate:
254 if repostate != self.repostate:
255 r = hg.repository(self.repo.baseui, self.repo.url())
255 r = hg.repository(self.repo.baseui, self.repo.url())
256 self.repo = getwebview(r)
256 self.repo = getwebview(r)
257 # update these last to avoid threads seeing empty settings
257 # update these last to avoid threads seeing empty settings
258 self.repostate = repostate
258 self.repostate = repostate
259 # mtime is needed for ETag
259 # mtime is needed for ETag
260 self.mtime = mtime
260 self.mtime = mtime
261
261
262 self.websubtable = webutil.getwebsubs(r)
262 self.websubtable = webutil.getwebsubs(r)
263
263
264 def run(self):
264 def run(self):
265 """Start a server from CGI environment.
265 """Start a server from CGI environment.
266
266
267 Modern servers should be using WSGI and should avoid this
267 Modern servers should be using WSGI and should avoid this
268 method, if possible.
268 method, if possible.
269 """
269 """
270 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
270 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
271 raise RuntimeError("This function is only intended to be "
271 raise RuntimeError("This function is only intended to be "
272 "called while running as a CGI script.")
272 "called while running as a CGI script.")
273 import mercurial.hgweb.wsgicgi as wsgicgi
273 import mercurial.hgweb.wsgicgi as wsgicgi
274 wsgicgi.launch(self)
274 wsgicgi.launch(self)
275
275
276 def __call__(self, env, respond):
276 def __call__(self, env, respond):
277 """Run the WSGI application.
277 """Run the WSGI application.
278
278
279 This may be called by multiple threads.
279 This may be called by multiple threads.
280 """
280 """
281 req = wsgirequest(env, respond)
281 req = wsgirequest(env, respond)
282 return self.run_wsgi(req)
282 return self.run_wsgi(req)
283
283
284 def run_wsgi(self, req):
284 def run_wsgi(self, req):
285 """Internal method to run the WSGI application.
285 """Internal method to run the WSGI application.
286
286
287 This is typically only called by Mercurial. External consumers
287 This is typically only called by Mercurial. External consumers
288 should be using instances of this class as the WSGI application.
288 should be using instances of this class as the WSGI application.
289 """
289 """
290 self.refresh()
290 self.refresh()
291 rctx = requestcontext(self)
291 rctx = requestcontext(self)
292
292
293 # This state is global across all threads.
293 # This state is global across all threads.
294 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
294 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
295 rctx.repo.ui.environ = req.env
295 rctx.repo.ui.environ = req.env
296
296
297 # work with CGI variables to create coherent structure
297 # work with CGI variables to create coherent structure
298 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
298 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
299
299
300 req.url = req.env['SCRIPT_NAME']
300 req.url = req.env['SCRIPT_NAME']
301 if not req.url.endswith('/'):
301 if not req.url.endswith('/'):
302 req.url += '/'
302 req.url += '/'
303 if 'REPO_NAME' in req.env:
303 if 'REPO_NAME' in req.env:
304 req.url += req.env['REPO_NAME'] + '/'
304 req.url += req.env['REPO_NAME'] + '/'
305
305
306 if 'PATH_INFO' in req.env:
306 if 'PATH_INFO' in req.env:
307 parts = req.env['PATH_INFO'].strip('/').split('/')
307 parts = req.env['PATH_INFO'].strip('/').split('/')
308 repo_parts = req.env.get('REPO_NAME', '').split('/')
308 repo_parts = req.env.get('REPO_NAME', '').split('/')
309 if parts[:len(repo_parts)] == repo_parts:
309 if parts[:len(repo_parts)] == repo_parts:
310 parts = parts[len(repo_parts):]
310 parts = parts[len(repo_parts):]
311 query = '/'.join(parts)
311 query = '/'.join(parts)
312 else:
312 else:
313 query = req.env['QUERY_STRING'].split('&', 1)[0]
313 query = req.env['QUERY_STRING'].split('&', 1)[0]
314 query = query.split(';', 1)[0]
314 query = query.split(';', 1)[0]
315
315
316 # process this if it's a protocol request
316 # process this if it's a protocol request
317 # protocol bits don't need to create any URLs
317 # protocol bits don't need to create any URLs
318 # and the clients always use the old URL structure
318 # and the clients always use the old URL structure
319
319
320 cmd = req.form.get('cmd', [''])[0]
320 cmd = req.form.get('cmd', [''])[0]
321 if protocol.iscmd(cmd):
321 if protocol.iscmd(cmd):
322 try:
322 try:
323 if query:
323 if query:
324 raise ErrorResponse(HTTP_NOT_FOUND)
324 raise ErrorResponse(HTTP_NOT_FOUND)
325 if cmd in perms:
325 if cmd in perms:
326 self.check_perm(rctx, req, perms[cmd])
326 self.check_perm(rctx, req, perms[cmd])
327 return protocol.call(self.repo, req, cmd)
327 return protocol.call(rctx.repo, req, cmd)
328 except ErrorResponse as inst:
328 except ErrorResponse as inst:
329 # A client that sends unbundle without 100-continue will
329 # A client that sends unbundle without 100-continue will
330 # break if we respond early.
330 # break if we respond early.
331 if (cmd == 'unbundle' and
331 if (cmd == 'unbundle' and
332 (req.env.get('HTTP_EXPECT',
332 (req.env.get('HTTP_EXPECT',
333 '').lower() != '100-continue') or
333 '').lower() != '100-continue') or
334 req.env.get('X-HgHttp2', '')):
334 req.env.get('X-HgHttp2', '')):
335 req.drain()
335 req.drain()
336 else:
336 else:
337 req.headers.append(('Connection', 'Close'))
337 req.headers.append(('Connection', 'Close'))
338 req.respond(inst, protocol.HGTYPE,
338 req.respond(inst, protocol.HGTYPE,
339 body='0\n%s\n' % inst)
339 body='0\n%s\n' % inst)
340 return ''
340 return ''
341
341
342 # translate user-visible url structure to internal structure
342 # translate user-visible url structure to internal structure
343
343
344 args = query.split('/', 2)
344 args = query.split('/', 2)
345 if 'cmd' not in req.form and args and args[0]:
345 if 'cmd' not in req.form and args and args[0]:
346
346
347 cmd = args.pop(0)
347 cmd = args.pop(0)
348 style = cmd.rfind('-')
348 style = cmd.rfind('-')
349 if style != -1:
349 if style != -1:
350 req.form['style'] = [cmd[:style]]
350 req.form['style'] = [cmd[:style]]
351 cmd = cmd[style + 1:]
351 cmd = cmd[style + 1:]
352
352
353 # avoid accepting e.g. style parameter as command
353 # avoid accepting e.g. style parameter as command
354 if util.safehasattr(webcommands, cmd):
354 if util.safehasattr(webcommands, cmd):
355 req.form['cmd'] = [cmd]
355 req.form['cmd'] = [cmd]
356
356
357 if cmd == 'static':
357 if cmd == 'static':
358 req.form['file'] = ['/'.join(args)]
358 req.form['file'] = ['/'.join(args)]
359 else:
359 else:
360 if args and args[0]:
360 if args and args[0]:
361 node = args.pop(0).replace('%2F', '/')
361 node = args.pop(0).replace('%2F', '/')
362 req.form['node'] = [node]
362 req.form['node'] = [node]
363 if args:
363 if args:
364 req.form['file'] = args
364 req.form['file'] = args
365
365
366 ua = req.env.get('HTTP_USER_AGENT', '')
366 ua = req.env.get('HTTP_USER_AGENT', '')
367 if cmd == 'rev' and 'mercurial' in ua:
367 if cmd == 'rev' and 'mercurial' in ua:
368 req.form['style'] = ['raw']
368 req.form['style'] = ['raw']
369
369
370 if cmd == 'archive':
370 if cmd == 'archive':
371 fn = req.form['node'][0]
371 fn = req.form['node'][0]
372 for type_, spec in rctx.archivespecs.iteritems():
372 for type_, spec in rctx.archivespecs.iteritems():
373 ext = spec[2]
373 ext = spec[2]
374 if fn.endswith(ext):
374 if fn.endswith(ext):
375 req.form['node'] = [fn[:-len(ext)]]
375 req.form['node'] = [fn[:-len(ext)]]
376 req.form['type'] = [type_]
376 req.form['type'] = [type_]
377
377
378 # process the web interface request
378 # process the web interface request
379
379
380 try:
380 try:
381 tmpl = rctx.templater(req)
381 tmpl = rctx.templater(req)
382 ctype = tmpl('mimetype', encoding=encoding.encoding)
382 ctype = tmpl('mimetype', encoding=encoding.encoding)
383 ctype = templater.stringify(ctype)
383 ctype = templater.stringify(ctype)
384
384
385 # check read permissions non-static content
385 # check read permissions non-static content
386 if cmd != 'static':
386 if cmd != 'static':
387 self.check_perm(rctx, req, None)
387 self.check_perm(rctx, req, None)
388
388
389 if cmd == '':
389 if cmd == '':
390 req.form['cmd'] = [tmpl.cache['default']]
390 req.form['cmd'] = [tmpl.cache['default']]
391 cmd = req.form['cmd'][0]
391 cmd = req.form['cmd'][0]
392
392
393 if rctx.configbool('web', 'cache', True):
393 if rctx.configbool('web', 'cache', True):
394 caching(self, req) # sets ETag header or raises NOT_MODIFIED
394 caching(self, req) # sets ETag header or raises NOT_MODIFIED
395 if cmd not in webcommands.__all__:
395 if cmd not in webcommands.__all__:
396 msg = 'no such method: %s' % cmd
396 msg = 'no such method: %s' % cmd
397 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
397 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
398 elif cmd == 'file' and 'raw' in req.form.get('style', []):
398 elif cmd == 'file' and 'raw' in req.form.get('style', []):
399 self.ctype = ctype
399 self.ctype = ctype
400 content = webcommands.rawfile(rctx, req, tmpl)
400 content = webcommands.rawfile(rctx, req, tmpl)
401 else:
401 else:
402 content = getattr(webcommands, cmd)(rctx, req, tmpl)
402 content = getattr(webcommands, cmd)(rctx, req, tmpl)
403 req.respond(HTTP_OK, ctype)
403 req.respond(HTTP_OK, ctype)
404
404
405 return content
405 return content
406
406
407 except (error.LookupError, error.RepoLookupError) as err:
407 except (error.LookupError, error.RepoLookupError) as err:
408 req.respond(HTTP_NOT_FOUND, ctype)
408 req.respond(HTTP_NOT_FOUND, ctype)
409 msg = str(err)
409 msg = str(err)
410 if (util.safehasattr(err, 'name') and
410 if (util.safehasattr(err, 'name') and
411 not isinstance(err, error.ManifestLookupError)):
411 not isinstance(err, error.ManifestLookupError)):
412 msg = 'revision not found: %s' % err.name
412 msg = 'revision not found: %s' % err.name
413 return tmpl('error', error=msg)
413 return tmpl('error', error=msg)
414 except (error.RepoError, error.RevlogError) as inst:
414 except (error.RepoError, error.RevlogError) as inst:
415 req.respond(HTTP_SERVER_ERROR, ctype)
415 req.respond(HTTP_SERVER_ERROR, ctype)
416 return tmpl('error', error=str(inst))
416 return tmpl('error', error=str(inst))
417 except ErrorResponse as inst:
417 except ErrorResponse as inst:
418 req.respond(inst, ctype)
418 req.respond(inst, ctype)
419 if inst.code == HTTP_NOT_MODIFIED:
419 if inst.code == HTTP_NOT_MODIFIED:
420 # Not allowed to return a body on a 304
420 # Not allowed to return a body on a 304
421 return ['']
421 return ['']
422 return tmpl('error', error=str(inst))
422 return tmpl('error', error=str(inst))
423
423
424 def check_perm(self, rctx, req, op):
424 def check_perm(self, rctx, req, op):
425 for permhook in permhooks:
425 for permhook in permhooks:
426 permhook(rctx, req, op)
426 permhook(rctx, req, op)
427
427
428 def getwebview(repo):
428 def getwebview(repo):
429 """The 'web.view' config controls changeset filter to hgweb. Possible
429 """The 'web.view' config controls changeset filter to hgweb. Possible
430 values are ``served``, ``visible`` and ``all``. Default is ``served``.
430 values are ``served``, ``visible`` and ``all``. Default is ``served``.
431 The ``served`` filter only shows changesets that can be pulled from the
431 The ``served`` filter only shows changesets that can be pulled from the
432 hgweb instance. The``visible`` filter includes secret changesets but
432 hgweb instance. The``visible`` filter includes secret changesets but
433 still excludes "hidden" one.
433 still excludes "hidden" one.
434
434
435 See the repoview module for details.
435 See the repoview module for details.
436
436
437 The option has been around undocumented since Mercurial 2.5, but no
437 The option has been around undocumented since Mercurial 2.5, but no
438 user ever asked about it. So we better keep it undocumented for now."""
438 user ever asked about it. So we better keep it undocumented for now."""
439 viewconfig = repo.ui.config('web', 'view', 'served',
439 viewconfig = repo.ui.config('web', 'view', 'served',
440 untrusted=True)
440 untrusted=True)
441 if viewconfig == 'all':
441 if viewconfig == 'all':
442 return repo.unfiltered()
442 return repo.unfiltered()
443 elif viewconfig in repoview.filtertable:
443 elif viewconfig in repoview.filtertable:
444 return repo.filtered(viewconfig)
444 return repo.filtered(viewconfig)
445 else:
445 else:
446 return repo.filtered('served')
446 return repo.filtered('served')
447
447
General Comments 0
You need to be logged in to leave comments. Login now