##// END OF EJS Templates
hgweb: use computed base URL from parsed request...
Gregory Szorc -
r36825:1e2194e0 default
parent child Browse files
Show More
@@ -1,466 +1,454 b''
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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 encoding,
27 encoding,
28 error,
28 error,
29 formatter,
29 formatter,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 pycompat,
33 pycompat,
34 repoview,
34 repoview,
35 templatefilters,
35 templatefilters,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 wireprotoserver,
39 wireprotoserver,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 request as requestmod,
43 request as requestmod,
44 webcommands,
44 webcommands,
45 webutil,
45 webutil,
46 wsgicgi,
46 wsgicgi,
47 )
47 )
48
48
49 archivespecs = util.sortdict((
49 archivespecs = util.sortdict((
50 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 ))
53 ))
54
54
55 def getstyle(req, configfn, templatepath):
55 def getstyle(req, configfn, templatepath):
56 fromreq = req.form.get('style', [None])[0]
56 fromreq = req.form.get('style', [None])[0]
57 styles = (
57 styles = (
58 fromreq,
58 fromreq,
59 configfn('web', 'style'),
59 configfn('web', 'style'),
60 'paper',
60 'paper',
61 )
61 )
62 return styles, templater.stylemap(styles, templatepath)
62 return styles, templater.stylemap(styles, templatepath)
63
63
64 def makebreadcrumb(url, prefix=''):
64 def makebreadcrumb(url, prefix=''):
65 '''Return a 'URL breadcrumb' list
65 '''Return a 'URL breadcrumb' list
66
66
67 A 'URL breadcrumb' is a list of URL-name pairs,
67 A 'URL breadcrumb' is a list of URL-name pairs,
68 corresponding to each of the path items on a URL.
68 corresponding to each of the path items on a URL.
69 This can be used to create path navigation entries.
69 This can be used to create path navigation entries.
70 '''
70 '''
71 if url.endswith('/'):
71 if url.endswith('/'):
72 url = url[:-1]
72 url = url[:-1]
73 if prefix:
73 if prefix:
74 url = '/' + prefix + url
74 url = '/' + prefix + url
75 relpath = url
75 relpath = url
76 if relpath.startswith('/'):
76 if relpath.startswith('/'):
77 relpath = relpath[1:]
77 relpath = relpath[1:]
78
78
79 breadcrumb = []
79 breadcrumb = []
80 urlel = url
80 urlel = url
81 pathitems = [''] + relpath.split('/')
81 pathitems = [''] + relpath.split('/')
82 for pathel in reversed(pathitems):
82 for pathel in reversed(pathitems):
83 if not pathel or not urlel:
83 if not pathel or not urlel:
84 break
84 break
85 breadcrumb.append({'url': urlel, 'name': pathel})
85 breadcrumb.append({'url': urlel, 'name': pathel})
86 urlel = os.path.dirname(urlel)
86 urlel = os.path.dirname(urlel)
87 return reversed(breadcrumb)
87 return reversed(breadcrumb)
88
88
89 class requestcontext(object):
89 class requestcontext(object):
90 """Holds state/context for an individual request.
90 """Holds state/context for an individual request.
91
91
92 Servers can be multi-threaded. Holding state on the WSGI application
92 Servers can be multi-threaded. Holding state on the WSGI application
93 is prone to race conditions. Instances of this class exist to hold
93 is prone to race conditions. Instances of this class exist to hold
94 mutable and race-free state for requests.
94 mutable and race-free state for requests.
95 """
95 """
96 def __init__(self, app, repo):
96 def __init__(self, app, repo):
97 self.repo = repo
97 self.repo = repo
98 self.reponame = app.reponame
98 self.reponame = app.reponame
99
99
100 self.archivespecs = archivespecs
100 self.archivespecs = archivespecs
101
101
102 self.maxchanges = self.configint('web', 'maxchanges')
102 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
103 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
105 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
106 self.allowpull = self.configbool('web', 'allow-pull')
107
107
108 # we use untrusted=False to prevent a repo owner from using
108 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
109 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
110 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
111 self.templatepath = self.config('web', 'templates', untrusted=False)
112
112
113 # This object is more expensive to build than simple config values.
113 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
114 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
115 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
116 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
117 # of the request.
118 self.websubtable = app.websubtable
118 self.websubtable = app.websubtable
119
119
120 self.csp, self.nonce = cspvalues(self.repo.ui)
120 self.csp, self.nonce = cspvalues(self.repo.ui)
121
121
122 # Trust the settings from the .hg/hgrc files by default.
122 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
124 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
125 untrusted=untrusted)
126
126
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
128 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
129 untrusted=untrusted)
130
130
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
132 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
133 untrusted=untrusted)
134
134
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
136 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
137 untrusted=untrusted)
138
138
139 def archivelist(self, nodeid):
139 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
144
145 def templater(self, wsgireq):
145 def templater(self, wsgireq, req):
146 # determine scheme, port and server name
146 # determine scheme, port and server name
147 # this is needed to create absolute urls
147 # this is needed to create absolute urls
148
149 proto = wsgireq.env.get('wsgi.url_scheme')
150 if proto == 'https':
151 proto = 'https'
152 default_port = '443'
153 else:
154 proto = 'http'
155 default_port = '80'
156
157 port = wsgireq.env[r'SERVER_PORT']
158 port = port != default_port and (r':' + port) or r''
159 urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port)
160 logourl = self.config('web', 'logourl')
148 logourl = self.config('web', 'logourl')
161 logoimg = self.config('web', 'logoimg')
149 logoimg = self.config('web', 'logoimg')
162 staticurl = (self.config('web', 'staticurl')
150 staticurl = (self.config('web', 'staticurl')
163 or pycompat.sysbytes(wsgireq.url) + 'static/')
151 or pycompat.sysbytes(wsgireq.url) + 'static/')
164 if not staticurl.endswith('/'):
152 if not staticurl.endswith('/'):
165 staticurl += '/'
153 staticurl += '/'
166
154
167 # some functions for the templater
155 # some functions for the templater
168
156
169 def motd(**map):
157 def motd(**map):
170 yield self.config('web', 'motd')
158 yield self.config('web', 'motd')
171
159
172 # figure out which style to use
160 # figure out which style to use
173
161
174 vars = {}
162 vars = {}
175 styles, (style, mapfile) = getstyle(wsgireq, self.config,
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
176 self.templatepath)
164 self.templatepath)
177 if style == styles[0]:
165 if style == styles[0]:
178 vars['style'] = style
166 vars['style'] = style
179
167
180 sessionvars = webutil.sessionvars(vars, '?')
168 sessionvars = webutil.sessionvars(vars, '?')
181
169
182 if not self.reponame:
170 if not self.reponame:
183 self.reponame = (self.config('web', 'name', '')
171 self.reponame = (self.config('web', 'name', '')
184 or wsgireq.env.get('REPO_NAME')
172 or wsgireq.env.get('REPO_NAME')
185 or wsgireq.url.strip(r'/') or self.repo.root)
173 or wsgireq.url.strip(r'/') or self.repo.root)
186
174
187 def websubfilter(text):
175 def websubfilter(text):
188 return templatefilters.websub(text, self.websubtable)
176 return templatefilters.websub(text, self.websubtable)
189
177
190 # create the templater
178 # create the templater
191 # TODO: export all keywords: defaults = templatekw.keywords.copy()
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 defaults = {
180 defaults = {
193 'url': pycompat.sysbytes(wsgireq.url),
181 'url': pycompat.sysbytes(wsgireq.url),
194 'logourl': logourl,
182 'logourl': logourl,
195 'logoimg': logoimg,
183 'logoimg': logoimg,
196 'staticurl': staticurl,
184 'staticurl': staticurl,
197 'urlbase': urlbase,
185 'urlbase': req.advertisedbaseurl,
198 'repo': self.reponame,
186 'repo': self.reponame,
199 'encoding': encoding.encoding,
187 'encoding': encoding.encoding,
200 'motd': motd,
188 'motd': motd,
201 'sessionvars': sessionvars,
189 'sessionvars': sessionvars,
202 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
190 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
203 'style': style,
191 'style': style,
204 'nonce': self.nonce,
192 'nonce': self.nonce,
205 }
193 }
206 tres = formatter.templateresources(self.repo.ui, self.repo)
194 tres = formatter.templateresources(self.repo.ui, self.repo)
207 tmpl = templater.templater.frommapfile(mapfile,
195 tmpl = templater.templater.frommapfile(mapfile,
208 filters={'websub': websubfilter},
196 filters={'websub': websubfilter},
209 defaults=defaults,
197 defaults=defaults,
210 resources=tres)
198 resources=tres)
211 return tmpl
199 return tmpl
212
200
213
201
214 class hgweb(object):
202 class hgweb(object):
215 """HTTP server for individual repositories.
203 """HTTP server for individual repositories.
216
204
217 Instances of this class serve HTTP responses for a particular
205 Instances of this class serve HTTP responses for a particular
218 repository.
206 repository.
219
207
220 Instances are typically used as WSGI applications.
208 Instances are typically used as WSGI applications.
221
209
222 Some servers are multi-threaded. On these servers, there may
210 Some servers are multi-threaded. On these servers, there may
223 be multiple active threads inside __call__.
211 be multiple active threads inside __call__.
224 """
212 """
225 def __init__(self, repo, name=None, baseui=None):
213 def __init__(self, repo, name=None, baseui=None):
226 if isinstance(repo, str):
214 if isinstance(repo, str):
227 if baseui:
215 if baseui:
228 u = baseui.copy()
216 u = baseui.copy()
229 else:
217 else:
230 u = uimod.ui.load()
218 u = uimod.ui.load()
231 r = hg.repository(u, repo)
219 r = hg.repository(u, repo)
232 else:
220 else:
233 # we trust caller to give us a private copy
221 # we trust caller to give us a private copy
234 r = repo
222 r = repo
235
223
236 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 # resolve file patterns relative to repo root
228 # resolve file patterns relative to repo root
241 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 # displaying bundling progress bar while serving feel wrong and may
231 # displaying bundling progress bar while serving feel wrong and may
244 # break some wsgi implementation.
232 # break some wsgi implementation.
245 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._lastrepo = self._repos[0]
236 self._lastrepo = self._repos[0]
249 hook.redirect(True)
237 hook.redirect(True)
250 self.reponame = name
238 self.reponame = name
251
239
252 def _webifyrepo(self, repo):
240 def _webifyrepo(self, repo):
253 repo = getwebview(repo)
241 repo = getwebview(repo)
254 self.websubtable = webutil.getwebsubs(repo)
242 self.websubtable = webutil.getwebsubs(repo)
255 return repo
243 return repo
256
244
257 @contextlib.contextmanager
245 @contextlib.contextmanager
258 def _obtainrepo(self):
246 def _obtainrepo(self):
259 """Obtain a repo unique to the caller.
247 """Obtain a repo unique to the caller.
260
248
261 Internally we maintain a stack of cachedlocalrepo instances
249 Internally we maintain a stack of cachedlocalrepo instances
262 to be handed out. If one is available, we pop it and return it,
250 to be handed out. If one is available, we pop it and return it,
263 ensuring it is up to date in the process. If one is not available,
251 ensuring it is up to date in the process. If one is not available,
264 we clone the most recently used repo instance and return it.
252 we clone the most recently used repo instance and return it.
265
253
266 It is currently possible for the stack to grow without bounds
254 It is currently possible for the stack to grow without bounds
267 if the server allows infinite threads. However, servers should
255 if the server allows infinite threads. However, servers should
268 have a thread limit, thus establishing our limit.
256 have a thread limit, thus establishing our limit.
269 """
257 """
270 if self._repos:
258 if self._repos:
271 cached = self._repos.pop()
259 cached = self._repos.pop()
272 r, created = cached.fetch()
260 r, created = cached.fetch()
273 else:
261 else:
274 cached = self._lastrepo.copy()
262 cached = self._lastrepo.copy()
275 r, created = cached.fetch()
263 r, created = cached.fetch()
276 if created:
264 if created:
277 r = self._webifyrepo(r)
265 r = self._webifyrepo(r)
278
266
279 self._lastrepo = cached
267 self._lastrepo = cached
280 self.mtime = cached.mtime
268 self.mtime = cached.mtime
281 try:
269 try:
282 yield r
270 yield r
283 finally:
271 finally:
284 self._repos.append(cached)
272 self._repos.append(cached)
285
273
286 def run(self):
274 def run(self):
287 """Start a server from CGI environment.
275 """Start a server from CGI environment.
288
276
289 Modern servers should be using WSGI and should avoid this
277 Modern servers should be using WSGI and should avoid this
290 method, if possible.
278 method, if possible.
291 """
279 """
292 if not encoding.environ.get('GATEWAY_INTERFACE',
280 if not encoding.environ.get('GATEWAY_INTERFACE',
293 '').startswith("CGI/1."):
281 '').startswith("CGI/1."):
294 raise RuntimeError("This function is only intended to be "
282 raise RuntimeError("This function is only intended to be "
295 "called while running as a CGI script.")
283 "called while running as a CGI script.")
296 wsgicgi.launch(self)
284 wsgicgi.launch(self)
297
285
298 def __call__(self, env, respond):
286 def __call__(self, env, respond):
299 """Run the WSGI application.
287 """Run the WSGI application.
300
288
301 This may be called by multiple threads.
289 This may be called by multiple threads.
302 """
290 """
303 req = requestmod.wsgirequest(env, respond)
291 req = requestmod.wsgirequest(env, respond)
304 return self.run_wsgi(req)
292 return self.run_wsgi(req)
305
293
306 def run_wsgi(self, wsgireq):
294 def run_wsgi(self, wsgireq):
307 """Internal method to run the WSGI application.
295 """Internal method to run the WSGI application.
308
296
309 This is typically only called by Mercurial. External consumers
297 This is typically only called by Mercurial. External consumers
310 should be using instances of this class as the WSGI application.
298 should be using instances of this class as the WSGI application.
311 """
299 """
312 with self._obtainrepo() as repo:
300 with self._obtainrepo() as repo:
313 profile = repo.ui.configbool('profiling', 'enabled')
301 profile = repo.ui.configbool('profiling', 'enabled')
314 with profiling.profile(repo.ui, enabled=profile):
302 with profiling.profile(repo.ui, enabled=profile):
315 for r in self._runwsgi(wsgireq, repo):
303 for r in self._runwsgi(wsgireq, repo):
316 yield r
304 yield r
317
305
318 def _runwsgi(self, wsgireq, repo):
306 def _runwsgi(self, wsgireq, repo):
319 req = requestmod.parserequestfromenv(wsgireq.env)
307 req = requestmod.parserequestfromenv(wsgireq.env)
320 rctx = requestcontext(self, repo)
308 rctx = requestcontext(self, repo)
321
309
322 # This state is global across all threads.
310 # This state is global across all threads.
323 encoding.encoding = rctx.config('web', 'encoding')
311 encoding.encoding = rctx.config('web', 'encoding')
324 rctx.repo.ui.environ = wsgireq.env
312 rctx.repo.ui.environ = wsgireq.env
325
313
326 if rctx.csp:
314 if rctx.csp:
327 # hgwebdir may have added CSP header. Since we generate our own,
315 # hgwebdir may have added CSP header. Since we generate our own,
328 # replace it.
316 # replace it.
329 wsgireq.headers = [h for h in wsgireq.headers
317 wsgireq.headers = [h for h in wsgireq.headers
330 if h[0] != 'Content-Security-Policy']
318 if h[0] != 'Content-Security-Policy']
331 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
332
320
333 wsgireq.url = pycompat.sysstr(req.apppath)
321 wsgireq.url = pycompat.sysstr(req.apppath)
334
322
335 if r'PATH_INFO' in wsgireq.env:
323 if r'PATH_INFO' in wsgireq.env:
336 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
324 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
337 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
325 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
338 if parts[:len(repo_parts)] == repo_parts:
326 if parts[:len(repo_parts)] == repo_parts:
339 parts = parts[len(repo_parts):]
327 parts = parts[len(repo_parts):]
340 query = r'/'.join(parts)
328 query = r'/'.join(parts)
341 else:
329 else:
342 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
330 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
343 query = query.partition(r';')[0]
331 query = query.partition(r';')[0]
344
332
345 # Route it to a wire protocol handler if it looks like a wire protocol
333 # Route it to a wire protocol handler if it looks like a wire protocol
346 # request.
334 # request.
347 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
335 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
348 self.check_perm)
336 self.check_perm)
349
337
350 if protohandler:
338 if protohandler:
351 try:
339 try:
352 if query:
340 if query:
353 raise ErrorResponse(HTTP_NOT_FOUND)
341 raise ErrorResponse(HTTP_NOT_FOUND)
354
342
355 return protohandler['dispatch']()
343 return protohandler['dispatch']()
356 except ErrorResponse as inst:
344 except ErrorResponse as inst:
357 return protohandler['handleerror'](inst)
345 return protohandler['handleerror'](inst)
358
346
359 # translate user-visible url structure to internal structure
347 # translate user-visible url structure to internal structure
360
348
361 args = query.split(r'/', 2)
349 args = query.split(r'/', 2)
362 if 'cmd' not in wsgireq.form and args and args[0]:
350 if 'cmd' not in wsgireq.form and args and args[0]:
363 cmd = args.pop(0)
351 cmd = args.pop(0)
364 style = cmd.rfind('-')
352 style = cmd.rfind('-')
365 if style != -1:
353 if style != -1:
366 wsgireq.form['style'] = [cmd[:style]]
354 wsgireq.form['style'] = [cmd[:style]]
367 cmd = cmd[style + 1:]
355 cmd = cmd[style + 1:]
368
356
369 # avoid accepting e.g. style parameter as command
357 # avoid accepting e.g. style parameter as command
370 if util.safehasattr(webcommands, cmd):
358 if util.safehasattr(webcommands, cmd):
371 wsgireq.form['cmd'] = [cmd]
359 wsgireq.form['cmd'] = [cmd]
372
360
373 if cmd == 'static':
361 if cmd == 'static':
374 wsgireq.form['file'] = ['/'.join(args)]
362 wsgireq.form['file'] = ['/'.join(args)]
375 else:
363 else:
376 if args and args[0]:
364 if args and args[0]:
377 node = args.pop(0).replace('%2F', '/')
365 node = args.pop(0).replace('%2F', '/')
378 wsgireq.form['node'] = [node]
366 wsgireq.form['node'] = [node]
379 if args:
367 if args:
380 wsgireq.form['file'] = args
368 wsgireq.form['file'] = args
381
369
382 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
370 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
383 if cmd == 'rev' and 'mercurial' in ua:
371 if cmd == 'rev' and 'mercurial' in ua:
384 wsgireq.form['style'] = ['raw']
372 wsgireq.form['style'] = ['raw']
385
373
386 if cmd == 'archive':
374 if cmd == 'archive':
387 fn = wsgireq.form['node'][0]
375 fn = wsgireq.form['node'][0]
388 for type_, spec in rctx.archivespecs.iteritems():
376 for type_, spec in rctx.archivespecs.iteritems():
389 ext = spec[2]
377 ext = spec[2]
390 if fn.endswith(ext):
378 if fn.endswith(ext):
391 wsgireq.form['node'] = [fn[:-len(ext)]]
379 wsgireq.form['node'] = [fn[:-len(ext)]]
392 wsgireq.form['type'] = [type_]
380 wsgireq.form['type'] = [type_]
393 else:
381 else:
394 cmd = wsgireq.form.get('cmd', [''])[0]
382 cmd = wsgireq.form.get('cmd', [''])[0]
395
383
396 # process the web interface request
384 # process the web interface request
397
385
398 try:
386 try:
399 tmpl = rctx.templater(wsgireq)
387 tmpl = rctx.templater(wsgireq, req)
400 ctype = tmpl('mimetype', encoding=encoding.encoding)
388 ctype = tmpl('mimetype', encoding=encoding.encoding)
401 ctype = templater.stringify(ctype)
389 ctype = templater.stringify(ctype)
402
390
403 # check read permissions non-static content
391 # check read permissions non-static content
404 if cmd != 'static':
392 if cmd != 'static':
405 self.check_perm(rctx, wsgireq, None)
393 self.check_perm(rctx, wsgireq, None)
406
394
407 if cmd == '':
395 if cmd == '':
408 wsgireq.form['cmd'] = [tmpl.cache['default']]
396 wsgireq.form['cmd'] = [tmpl.cache['default']]
409 cmd = wsgireq.form['cmd'][0]
397 cmd = wsgireq.form['cmd'][0]
410
398
411 # Don't enable caching if using a CSP nonce because then it wouldn't
399 # Don't enable caching if using a CSP nonce because then it wouldn't
412 # be a nonce.
400 # be a nonce.
413 if rctx.configbool('web', 'cache') and not rctx.nonce:
401 if rctx.configbool('web', 'cache') and not rctx.nonce:
414 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
402 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
415 if cmd not in webcommands.__all__:
403 if cmd not in webcommands.__all__:
416 msg = 'no such method: %s' % cmd
404 msg = 'no such method: %s' % cmd
417 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
405 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
418 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
406 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
419 rctx.ctype = ctype
407 rctx.ctype = ctype
420 content = webcommands.rawfile(rctx, wsgireq, tmpl)
408 content = webcommands.rawfile(rctx, wsgireq, tmpl)
421 else:
409 else:
422 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
410 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
423 wsgireq.respond(HTTP_OK, ctype)
411 wsgireq.respond(HTTP_OK, ctype)
424
412
425 return content
413 return content
426
414
427 except (error.LookupError, error.RepoLookupError) as err:
415 except (error.LookupError, error.RepoLookupError) as err:
428 wsgireq.respond(HTTP_NOT_FOUND, ctype)
416 wsgireq.respond(HTTP_NOT_FOUND, ctype)
429 msg = pycompat.bytestr(err)
417 msg = pycompat.bytestr(err)
430 if (util.safehasattr(err, 'name') and
418 if (util.safehasattr(err, 'name') and
431 not isinstance(err, error.ManifestLookupError)):
419 not isinstance(err, error.ManifestLookupError)):
432 msg = 'revision not found: %s' % err.name
420 msg = 'revision not found: %s' % err.name
433 return tmpl('error', error=msg)
421 return tmpl('error', error=msg)
434 except (error.RepoError, error.RevlogError) as inst:
422 except (error.RepoError, error.RevlogError) as inst:
435 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
423 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
436 return tmpl('error', error=pycompat.bytestr(inst))
424 return tmpl('error', error=pycompat.bytestr(inst))
437 except ErrorResponse as inst:
425 except ErrorResponse as inst:
438 wsgireq.respond(inst, ctype)
426 wsgireq.respond(inst, ctype)
439 if inst.code == HTTP_NOT_MODIFIED:
427 if inst.code == HTTP_NOT_MODIFIED:
440 # Not allowed to return a body on a 304
428 # Not allowed to return a body on a 304
441 return ['']
429 return ['']
442 return tmpl('error', error=pycompat.bytestr(inst))
430 return tmpl('error', error=pycompat.bytestr(inst))
443
431
444 def check_perm(self, rctx, req, op):
432 def check_perm(self, rctx, req, op):
445 for permhook in permhooks:
433 for permhook in permhooks:
446 permhook(rctx, req, op)
434 permhook(rctx, req, op)
447
435
448 def getwebview(repo):
436 def getwebview(repo):
449 """The 'web.view' config controls changeset filter to hgweb. Possible
437 """The 'web.view' config controls changeset filter to hgweb. Possible
450 values are ``served``, ``visible`` and ``all``. Default is ``served``.
438 values are ``served``, ``visible`` and ``all``. Default is ``served``.
451 The ``served`` filter only shows changesets that can be pulled from the
439 The ``served`` filter only shows changesets that can be pulled from the
452 hgweb instance. The``visible`` filter includes secret changesets but
440 hgweb instance. The``visible`` filter includes secret changesets but
453 still excludes "hidden" one.
441 still excludes "hidden" one.
454
442
455 See the repoview module for details.
443 See the repoview module for details.
456
444
457 The option has been around undocumented since Mercurial 2.5, but no
445 The option has been around undocumented since Mercurial 2.5, but no
458 user ever asked about it. So we better keep it undocumented for now."""
446 user ever asked about it. So we better keep it undocumented for now."""
459 # experimental config: web.view
447 # experimental config: web.view
460 viewconfig = repo.ui.config('web', 'view', untrusted=True)
448 viewconfig = repo.ui.config('web', 'view', untrusted=True)
461 if viewconfig == 'all':
449 if viewconfig == 'all':
462 return repo.unfiltered()
450 return repo.unfiltered()
463 elif viewconfig in repoview.filtertable:
451 elif viewconfig in repoview.filtertable:
464 return repo.filtered(viewconfig)
452 return repo.filtered(viewconfig)
465 else:
453 else:
466 return repo.filtered('served')
454 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now