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